Compare commits
34 Commits
Author | SHA1 | Date | |
---|---|---|---|
91880f8d42 | |||
7b1732abcc | |||
7d09b39f2b | |||
96efba5903 | |||
3c535a8a77 | |||
0954265095 | |||
e1d90589bc | |||
33f705d961 | |||
13b11ab1bf | |||
63280e4a9a | |||
23addc2d2f | |||
3649114c8d | |||
2841aba8a4 | |||
31bf090410 | |||
b525754035 | |||
aa10fc4ab3 | |||
3eb8ef22e5 | |||
763dc89f59 | |||
e0d8ede450 | |||
27c950c1a1 | |||
83b324b09f | |||
63a2879cb4 | |||
1a375fa689 | |||
c48887a820 | |||
02aeb8195e | |||
53d3dc55e6 | |||
a82fdc0f26 | |||
cfcb99de76 | |||
a3a4ded41e | |||
03d478d6ff | |||
77e53bd68a | |||
946e467c26 | |||
f452a58fff | |||
2b01d949f2 |
2
.gitignore
vendored
2
.gitignore
vendored
@ -17,4 +17,4 @@ node_modules/
|
||||
dist/
|
||||
dist_*/
|
||||
|
||||
# custom
|
||||
# custom
|
||||
|
134
changelog.md
134
changelog.md
@ -1,5 +1,139 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-05-25 - 2.0.0 - BREAKING CHANGE(protocol)
|
||||
Introduce protocol v2 implementation and update build configuration with revised build order, new tspublish files, and enhanced documentation
|
||||
|
||||
- Added ts_tapbundle_protocol directory with isomorphic implementation for protocol v2
|
||||
- Updated readme.hints.md and readme.plan.md to explain the complete replacement of the v1 protocol and new build process
|
||||
- Revised build order in tspublish.json files across ts, ts_tapbundle, ts_tapbundle_node, and ts_tapbundle_protocol
|
||||
- Introduced .claude/settings.local.json with updated permission settings for CLI and build tools
|
||||
|
||||
## 2025-05-24 - 1.11.5 - fix(tstest)
|
||||
Fix timeout handling to correctly evaluate TAP results after killing the test process.
|
||||
|
||||
- Added call to evaluateFinalResult() after killing the process in runInNode to ensure final TAP output is processed.
|
||||
|
||||
## 2025-05-24 - 1.11.4 - fix(logging)
|
||||
Improve warning logging and add permission settings file
|
||||
|
||||
- Replace multiple logger.error calls with logger.warning for tests running over 1 minute
|
||||
- Add warning method in tstest logger to display warning messages consistently
|
||||
- Introduce .claude/settings.local.json to configure allowed permissions
|
||||
|
||||
## 2025-05-24 - 1.11.3 - fix(tstest)
|
||||
Add timeout warning for long-running tests and introduce local settings configuration
|
||||
|
||||
- Add .claude/settings.local.json with permission configuration for local development
|
||||
- Implement a timeout warning timer that notifies when tests run longer than 1 minute without an explicit timeout
|
||||
- Clear the timeout warning timer upon test completion
|
||||
- Remove unused import of logPrefixes in tstest.classes.tstest.ts
|
||||
|
||||
## 2025-05-24 - 1.11.2 - fix(tstest)
|
||||
Improve timeout and error handling in test execution along with TAP parser timeout logic improvements.
|
||||
|
||||
- In the TAP parser, ensure that expected tests are properly set when no tests are defined to avoid false negatives on timeout.
|
||||
- Use smartshell's terminate method and fallback kill to properly stop the entire process tree on timeout.
|
||||
- Clean up browser, server, and WebSocket instances reliably even when a timeout occurs.
|
||||
- Minor improvements in log file filtering and error logging for better clarity.
|
||||
|
||||
## 2025-05-24 - 1.11.1 - fix(tstest)
|
||||
Clear timeout identifiers after successful test execution and add local CLAUDE settings
|
||||
|
||||
- Ensure timeout IDs are cleared when tests complete to prevent lingering timeouts
|
||||
- Add .claude/settings.local.json with updated permission settings for CLI commands
|
||||
|
||||
## 2025-05-24 - 1.11.0 - feat(cli)
|
||||
Add new timeout and file range options with enhanced logfile diff logging
|
||||
|
||||
- Introduce --timeout <seconds> option to safeguard tests from running too long
|
||||
- Add --startFrom and --stopAt options to control the range of test files executed
|
||||
- Enhance logfile organization by automatically moving previous logs and generating diff reports for failed or changed test outputs
|
||||
- Update CLI argument parsing and internal timeout handling for both Node.js and browser tests
|
||||
|
||||
## 2025-05-24 - 1.10.2 - fix(tstest-logging)
|
||||
Improve log file handling with log rotation and diff reporting
|
||||
|
||||
- Add .claude/settings.local.json to configure allowed shell and web operations
|
||||
- Introduce movePreviousLogFiles function to archive previous log files when --logfile is used
|
||||
- Enhance logging to generate error copies and diff reports between current and previous logs
|
||||
- Add type annotations for console overrides in browser evaluations for improved stability
|
||||
|
||||
## 2025-05-23 - 1.10.1 - fix(tstest)
|
||||
Improve file range filtering and summary logging by skipping test files outside the specified range and reporting them in the final summary.
|
||||
|
||||
- Introduce runSingleTestOrSkip to check file index against startFrom/stopAt values.
|
||||
- Log skipped files with appropriate messages and add them to the summary.
|
||||
- Update the logger to include total skipped files in the test summary.
|
||||
- Add permission settings in .claude/settings.local.json to support new operations.
|
||||
|
||||
## 2025-05-23 - 1.10.0 - feat(cli)
|
||||
Add --startFrom and --stopAt options to filter test files by range
|
||||
|
||||
- Introduced CLI options --startFrom and --stopAt in ts/index.ts for selective test execution
|
||||
- Added validation to ensure provided range values are positive and startFrom is not greater than stopAt
|
||||
- Propagated file range filtering into test grouping in tstest.classes.tstest.ts, applying the range filter across serial and parallel groups
|
||||
- Updated usage messages to include the new options
|
||||
|
||||
## 2025-05-23 - 1.9.4 - fix(docs)
|
||||
Update documentation and configuration for legal notices and CI permissions. This commit adds a new local settings file for tool permissions, refines the legal and trademark sections in the readme, and improves glob test files with clearer log messages.
|
||||
|
||||
- Added .claude/settings.local.json to configure permissions for various CLI commands
|
||||
- Revised legal and trademark documentation in the readme to clarify company ownership and usage guidelines
|
||||
- Updated glob test files with improved console log messages for better clarity during test discovery
|
||||
|
||||
## 2025-05-23 - 1.9.3 - fix(tstest)
|
||||
Fix test timing display issue and update TAP protocol documentation
|
||||
|
||||
- Changed TAP parser regex to non-greedy pattern to correctly separate test timing metadata
|
||||
- Enhanced readme.hints.md with detailed explanation of test timing fix and planned protocol upgrades
|
||||
- Updated readme.md with improved usage examples for tapbundle and comprehensive test framework documentation
|
||||
- Added new protocol design document (readme.protocol.md) and improvement plan (readme.plan.md) outlining future changes
|
||||
- Introduced .claude/settings.local.json update for npm and CLI permissions
|
||||
- Exported protocol utilities and added tapbundle protocol implementation for future enhancements
|
||||
|
||||
## 2025-05-23 - 1.9.2 - fix(logging)
|
||||
Fix log file naming to prevent collisions and update logging system documentation.
|
||||
|
||||
- Enhance safe filename generation in tstest logging to preserve directory structure using double underscores.
|
||||
- Update readme.hints.md to include detailed logging system documentation and behavior.
|
||||
- Add .claude/settings.local.json with updated permissions for build tools.
|
||||
|
||||
## 2025-05-23 - 1.9.1 - fix(dependencies)
|
||||
Update dependency versions and add local configuration files
|
||||
|
||||
- Bump @git.zone/tsbuild from ^2.5.1 to ^2.6.3
|
||||
- Bump @types/node from ^22.15.18 to ^22.15.21
|
||||
- Bump @push.rocks/smartexpect from ^2.4.2 to ^2.5.0
|
||||
- Bump @push.rocks/smartfile from ^11.2.0 to ^11.2.3
|
||||
- Bump @push.rocks/smartlog from ^3.1.1 to ^3.1.8
|
||||
- Add .npmrc with npm registry configuration
|
||||
- Add .claude/settings.local.json for local permissions
|
||||
|
||||
## 2025-05-16 - 1.9.0 - feat(docs)
|
||||
Update documentation to embed tapbundle and clarify module exports for browser compatibility; also add CI permission settings.
|
||||
|
||||
- Embed tapbundle directly into tstest to simplify usage and ensure browser support.
|
||||
- Update import paths in examples from '@push.rocks/tapbundle' to '@git.zone/tstest/tapbundle'.
|
||||
- Revise the changelog to reflect version 1.8.0 improvements including enhanced test lifecycle hooks and parallel execution fixes.
|
||||
- Add .claude/settings.local.json to configure CI-related permissions and tool operations.
|
||||
|
||||
## 2025-05-16 - 1.8.0 - feat(documentation)
|
||||
Enhance README with detailed test features and update local settings for build permissions.
|
||||
|
||||
- Expanded the documentation to include tag filtering, parallel test execution groups, lifecycle hooks, snapshot testing, timeout control, retry logic, and test fixtures
|
||||
- Updated .claude/settings.local.json to allow additional permissions for various build and test commands
|
||||
|
||||
## 2025-05-16 - 1.7.0 - feat(tstest)
|
||||
Enhance tstest with fluent API, suite grouping, tag filtering, fixture & snapshot testing, and parallel execution improvements
|
||||
|
||||
- Updated npm scripts to run tests in verbose mode and support glob patterns with quotes
|
||||
- Introduced tag filtering support (--tags) in the CLI to run tests by specified tags
|
||||
- Implemented fluent syntax methods (tags, priority, retry, timeout) for defining tests and applying settings
|
||||
- Added test suite grouping with describe(), along with beforeEach and afterEach lifecycle hooks
|
||||
- Integrated a fixture system and snapshot testing via TapTools with base64 snapshot communication
|
||||
- Enhanced TAP parser regex, error collection, and snapshot handling for improved debugging
|
||||
- Improved parallel test execution by grouping files with a 'para__' pattern and running them concurrently
|
||||
|
||||
## 2025-05-15 - 1.6.0 - feat(package)
|
||||
Revamp package exports and update permissions with an extensive improvement plan for test runner enhancements.
|
||||
|
||||
|
19
license
Normal file
19
license
Normal file
@ -0,0 +1,19 @@
|
||||
Copyright (c) 2014 Task Venture Capital GmbH (hello@task.vc)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
22
package.json
22
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@git.zone/tstest",
|
||||
"version": "1.6.0",
|
||||
"version": "2.0.0",
|
||||
"private": false,
|
||||
"description": "a test utility to run tests that match test/**/*.ts",
|
||||
"exports": {
|
||||
@ -15,17 +15,17 @@
|
||||
"tstest": "./cli.js"
|
||||
},
|
||||
"scripts": {
|
||||
"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",
|
||||
"test": "pnpm run build && pnpm run test:tapbundle:verbose && pnpm run test:tstest:verbose",
|
||||
"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.5.1",
|
||||
"@types/node": "^22.15.18"
|
||||
"@git.zone/tsbuild": "^2.6.3",
|
||||
"@types/node": "^22.15.21"
|
||||
},
|
||||
"dependencies": {
|
||||
"@api.global/typedserver": "^3.0.74",
|
||||
@ -37,10 +37,10 @@
|
||||
"@push.rocks/smartcrypto": "^2.0.4",
|
||||
"@push.rocks/smartdelay": "^3.0.5",
|
||||
"@push.rocks/smartenv": "^5.0.12",
|
||||
"@push.rocks/smartexpect": "^2.4.2",
|
||||
"@push.rocks/smartfile": "^11.2.0",
|
||||
"@push.rocks/smartexpect": "^2.5.0",
|
||||
"@push.rocks/smartfile": "^11.2.3",
|
||||
"@push.rocks/smartjson": "^5.0.20",
|
||||
"@push.rocks/smartlog": "^3.1.1",
|
||||
"@push.rocks/smartlog": "^3.1.8",
|
||||
"@push.rocks/smartmongo": "^2.0.12",
|
||||
"@push.rocks/smartpath": "^5.0.18",
|
||||
"@push.rocks/smartpromise": "^4.2.3",
|
||||
|
167
pnpm-lock.yaml
generated
167
pnpm-lock.yaml
generated
@ -36,17 +36,17 @@ importers:
|
||||
specifier: ^5.0.12
|
||||
version: 5.0.12
|
||||
'@push.rocks/smartexpect':
|
||||
specifier: ^2.4.2
|
||||
version: 2.4.2
|
||||
specifier: ^2.5.0
|
||||
version: 2.5.0
|
||||
'@push.rocks/smartfile':
|
||||
specifier: ^11.2.0
|
||||
version: 11.2.0
|
||||
specifier: ^11.2.3
|
||||
version: 11.2.3
|
||||
'@push.rocks/smartjson':
|
||||
specifier: ^5.0.20
|
||||
version: 5.0.20
|
||||
'@push.rocks/smartlog':
|
||||
specifier: ^3.1.1
|
||||
version: 3.1.1
|
||||
specifier: ^3.1.8
|
||||
version: 3.1.8
|
||||
'@push.rocks/smartmongo':
|
||||
specifier: ^2.0.12
|
||||
version: 2.0.12(@aws-sdk/credential-providers@3.810.0)(socks@2.8.4)
|
||||
@ -79,11 +79,11 @@ importers:
|
||||
version: 8.18.2
|
||||
devDependencies:
|
||||
'@git.zone/tsbuild':
|
||||
specifier: ^2.5.1
|
||||
version: 2.5.1
|
||||
specifier: ^2.6.3
|
||||
version: 2.6.3
|
||||
'@types/node':
|
||||
specifier: ^22.15.18
|
||||
version: 22.15.18
|
||||
specifier: ^22.15.21
|
||||
version: 22.15.21
|
||||
|
||||
packages:
|
||||
|
||||
@ -585,8 +585,8 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@git.zone/tsbuild@2.5.1':
|
||||
resolution: {integrity: sha512-b1TyaNnaPCD3dvdRZ2da0MkZbH9liCrhzg57pwFIB2Gx4g8UMv8ZLN2cA1NRaNE0o8NCybf3gV1L+V0FO0DrMQ==}
|
||||
'@git.zone/tsbuild@2.6.3':
|
||||
resolution: {integrity: sha512-KIJYGQf9g5YibQZFWniYhESi7cWDZyRiudrYyipEQdyrv0o4VwXCdFgvsi90EZyoR2gdvz9qIWKeB1VaGx/dcQ==}
|
||||
hasBin: true
|
||||
|
||||
'@git.zone/tsbundle@2.2.5':
|
||||
@ -732,8 +732,8 @@ packages:
|
||||
'@push.rocks/smartexit@1.0.23':
|
||||
resolution: {integrity: sha512-WmwKYcwbHBByoABhHHB+PAjr5475AtD/xBh1mDcqPrFsOOUOZq3BBUdpq25wI3ccu/SZB5IwaimiVzadls6HkA==}
|
||||
|
||||
'@push.rocks/smartexpect@2.4.2':
|
||||
resolution: {integrity: sha512-L+aS1n5rWhf/yOh5R3zPgwycYtDr5FfrDWgasy6ShhN6Zbn/z/AOPbWcF/OpeTmx0XabWB2h5d4xBcCKLl47cQ==}
|
||||
'@push.rocks/smartexpect@2.5.0':
|
||||
resolution: {integrity: sha512-yoyuCoQ3tTiAriuvF+/09fNbVfFnacudL2SwHSzPhX/ugaE7VTSWXQ9A34eKOWvil0MPyDcOY36fVZDxvrPd8A==}
|
||||
|
||||
'@push.rocks/smartfeed@1.0.11':
|
||||
resolution: {integrity: sha512-02uhXxQamgfBo3T12FsAdfyElnpoWuDUb08B2AE60DbIaukVx/7Mi17xwobApY1flNSr5StZDt8N8vxPhBhIXw==}
|
||||
@ -744,8 +744,8 @@ packages:
|
||||
'@push.rocks/smartfile@10.0.41':
|
||||
resolution: {integrity: sha512-xOOy0duI34M2qrJZggpk51EHGXmg9+mBL1Q55tNiQKXzfx89P3coY1EAZG8tvmep3qB712QEKe7T+u04t42Kjg==}
|
||||
|
||||
'@push.rocks/smartfile@11.2.0':
|
||||
resolution: {integrity: sha512-0Gw6DvCQ2D/BXNN6airSC7hoSBut0p/uNWf2+rqO+D6VLhIJ/QUBvF6xm/LnpPI/zcF8YlDn/GEriInB5DUtEw==}
|
||||
'@push.rocks/smartfile@11.2.3':
|
||||
resolution: {integrity: sha512-gXUCwzHE6TuuzQIRGuZhJhPZJcVyc4G9nll32LHgmnBAU5ynDsGWUUbtFmpgcYLSAYFM9LGZS4b+ZrQPoDrtJw==}
|
||||
|
||||
'@push.rocks/smartguard@3.1.0':
|
||||
resolution: {integrity: sha512-J23q84f1O+TwFGmd4lrO9XLHUh2DaLXo9PN/9VmTWYzTkQDv5JehmifXVI0esophXcCIfbdIu6hbt7/aHlDF4A==}
|
||||
@ -765,8 +765,8 @@ packages:
|
||||
'@push.rocks/smartlog-interfaces@3.0.2':
|
||||
resolution: {integrity: sha512-8hGRTJehbsFSJxLhCQkA018mZtXVPxPTblbg9VaE/EqISRzUw+eosJ2EJV7M4Qu0eiTJZjnWnNLn8CkD77ziWw==}
|
||||
|
||||
'@push.rocks/smartlog@3.1.1':
|
||||
resolution: {integrity: sha512-bANAjbPUty6jncut3FKHcSfrU6lm/gNW27q+BrqKH3I71qYWuh9HR0+tgJz+Ha47/sONzCmLcX/I2VejZ3njJg==}
|
||||
'@push.rocks/smartlog@3.1.8':
|
||||
resolution: {integrity: sha512-j4H5x4/hEmiIO7q+/LKyX3N+AhRIOj1jDE4TvZDvujZkbT/9wEWfpO1bqeMe/EQbg1eOQMlAuyrcLXUcDICpQg==}
|
||||
|
||||
'@push.rocks/smartmanifest@2.0.2':
|
||||
resolution: {integrity: sha512-QGc5C9vunjfUbYsPGz5bynV/mVmPHkrQDkWp8ZO8VJtK1GZe+njgbrNyxn2SUHR0IhSAbSXl1j4JvBqYf5eTVg==}
|
||||
@ -1353,8 +1353,8 @@ packages:
|
||||
'@types/node-forge@1.3.11':
|
||||
resolution: {integrity: sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==}
|
||||
|
||||
'@types/node@22.15.18':
|
||||
resolution: {integrity: sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg==}
|
||||
'@types/node@22.15.21':
|
||||
resolution: {integrity: sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==}
|
||||
|
||||
'@types/ping@0.4.4':
|
||||
resolution: {integrity: sha512-ifvo6w2f5eJYlXm+HiVx67iJe8WZp87sfa683nlqED5Vnt9Z93onkokNoWqOG21EaE8fMxyKPobE+mkPEyxsdw==}
|
||||
@ -1541,7 +1541,7 @@ packages:
|
||||
resolution: {integrity: sha512-InJljddsYWbEL8LBnopnCg+qMQp9KcowvYWOt4YWrjD5HmxzDYKdVbDS1w/ji5rFZdRD58V5UxJPtBdpEbEJYw==}
|
||||
|
||||
browserify-zlib@0.1.4:
|
||||
resolution: {integrity: sha1-uzX4pRn2AOD6a4SFJByXnQFB+y0=}
|
||||
resolution: {integrity: sha512-19OEpq7vWgsH6WkvkBJQDFvJS1uPcbFOQ4v9CU839dO+ZZXUZO6XpE6hNCqvlIIj+4fZvRiJ6DsAQ382GwiyTQ==}
|
||||
|
||||
bson@4.7.2:
|
||||
resolution: {integrity: sha512-Ry9wCtIZ5kGqkJoi6aD8KjxFZEx78guTQDnpXWiNthsxzrxAK/i8E6pCHAIZTbaEFWcOCvbecMukfK7XUvyLpQ==}
|
||||
@ -2103,7 +2103,7 @@ packages:
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
from2@2.3.0:
|
||||
resolution: {integrity: sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=}
|
||||
resolution: {integrity: sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==}
|
||||
|
||||
fs-constants@1.0.0:
|
||||
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
|
||||
@ -2309,7 +2309,7 @@ packages:
|
||||
resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==}
|
||||
|
||||
is-deflate@1.0.0:
|
||||
resolution: {integrity: sha1-yGKQHDwWH7CdrHzcfnhPgOmPLxQ=}
|
||||
resolution: {integrity: sha512-YDoFpuZWu1VRXlsnlYMzKyVRITXj7Ej/V9gXQ2/pAe7X1J7M/RNOqaIYi6qUn+B7nGyB9pDXrv02dsB58d2ZAQ==}
|
||||
|
||||
is-docker@2.2.1:
|
||||
resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==}
|
||||
@ -2325,7 +2325,7 @@ packages:
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
is-gzip@1.0.0:
|
||||
resolution: {integrity: sha1-bKiwe5nHeZgCWQDlVc7Y7YCHmoM=}
|
||||
resolution: {integrity: sha512-rcfALRIb1YewtnksfRIHGcIY93QnK8BIQ/2c9yDYcG/Y6+vRoJuTWBmmSEbyLLYtXm7q35pHOHbZFQBaLrhlWQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
is-ip@4.0.0:
|
||||
@ -2373,7 +2373,7 @@ packages:
|
||||
engines: {node: '>=8'}
|
||||
|
||||
isarray@1.0.0:
|
||||
resolution: {integrity: sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=}
|
||||
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
|
||||
|
||||
isexe@2.0.0:
|
||||
resolution: {integrity: sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=}
|
||||
@ -2388,8 +2388,8 @@ packages:
|
||||
jackspeak@3.4.3:
|
||||
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
|
||||
|
||||
jackspeak@4.1.0:
|
||||
resolution: {integrity: sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw==}
|
||||
jackspeak@4.1.1:
|
||||
resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
js-base64@3.7.7:
|
||||
@ -2933,7 +2933,7 @@ packages:
|
||||
engines: {node: '>=14.16'}
|
||||
|
||||
pako@0.2.9:
|
||||
resolution: {integrity: sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU=}
|
||||
resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==}
|
||||
|
||||
pako@1.0.11:
|
||||
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
|
||||
@ -3045,7 +3045,7 @@ packages:
|
||||
resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==}
|
||||
|
||||
proto-list@1.2.4:
|
||||
resolution: {integrity: sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=}
|
||||
resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==}
|
||||
|
||||
proxy-addr@2.0.7:
|
||||
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
|
||||
@ -3350,7 +3350,7 @@ packages:
|
||||
engines: {node: '>=12'}
|
||||
|
||||
strip-json-comments@2.0.1:
|
||||
resolution: {integrity: sha1-PFMZQukIwml8DsNEhYwobHygpgo=}
|
||||
resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
strnum@1.1.2:
|
||||
@ -3489,6 +3489,11 @@ packages:
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
typescript@5.8.3:
|
||||
resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==}
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
uglify-js@3.19.3:
|
||||
resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==}
|
||||
engines: {node: '>=0.8.0'}
|
||||
@ -3734,9 +3739,9 @@ snapshots:
|
||||
'@push.rocks/smartdelay': 3.0.5
|
||||
'@push.rocks/smartenv': 5.0.12
|
||||
'@push.rocks/smartfeed': 1.0.11
|
||||
'@push.rocks/smartfile': 11.2.0
|
||||
'@push.rocks/smartfile': 11.2.3
|
||||
'@push.rocks/smartjson': 5.0.20
|
||||
'@push.rocks/smartlog': 3.1.1
|
||||
'@push.rocks/smartlog': 3.1.8
|
||||
'@push.rocks/smartlog-destination-devtools': 1.0.12
|
||||
'@push.rocks/smartlog-interfaces': 3.0.2
|
||||
'@push.rocks/smartmanifest': 2.0.2
|
||||
@ -4536,17 +4541,17 @@ snapshots:
|
||||
'@esbuild/win32-x64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@git.zone/tsbuild@2.5.1':
|
||||
'@git.zone/tsbuild@2.6.3':
|
||||
dependencies:
|
||||
'@git.zone/tspublish': 1.9.1
|
||||
'@push.rocks/early': 4.0.4
|
||||
'@push.rocks/smartcli': 4.0.11
|
||||
'@push.rocks/smartdelay': 3.0.5
|
||||
'@push.rocks/smartfile': 11.2.0
|
||||
'@push.rocks/smartlog': 3.1.1
|
||||
'@push.rocks/smartfile': 11.2.3
|
||||
'@push.rocks/smartlog': 3.1.8
|
||||
'@push.rocks/smartpath': 5.0.18
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
typescript: 5.7.3
|
||||
typescript: 5.8.3
|
||||
transitivePeerDependencies:
|
||||
- aws-crt
|
||||
|
||||
@ -4555,8 +4560,8 @@ snapshots:
|
||||
'@push.rocks/early': 4.0.4
|
||||
'@push.rocks/smartcli': 4.0.11
|
||||
'@push.rocks/smartdelay': 3.0.5
|
||||
'@push.rocks/smartfile': 11.2.0
|
||||
'@push.rocks/smartlog': 3.1.1
|
||||
'@push.rocks/smartfile': 11.2.3
|
||||
'@push.rocks/smartlog': 3.1.8
|
||||
'@push.rocks/smartlog-destination-local': 9.0.2
|
||||
'@push.rocks/smartpath': 5.0.18
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
@ -4572,8 +4577,8 @@ snapshots:
|
||||
dependencies:
|
||||
'@push.rocks/smartcli': 4.0.11
|
||||
'@push.rocks/smartdelay': 3.0.5
|
||||
'@push.rocks/smartfile': 11.2.0
|
||||
'@push.rocks/smartlog': 3.1.1
|
||||
'@push.rocks/smartfile': 11.2.3
|
||||
'@push.rocks/smartlog': 3.1.8
|
||||
'@push.rocks/smartnpm': 2.0.4
|
||||
'@push.rocks/smartpath': 5.0.18
|
||||
'@push.rocks/smartrequest': 2.1.0
|
||||
@ -4583,7 +4588,7 @@ snapshots:
|
||||
|
||||
'@git.zone/tsrun@1.3.3':
|
||||
dependencies:
|
||||
'@push.rocks/smartfile': 11.2.0
|
||||
'@push.rocks/smartfile': 11.2.3
|
||||
'@push.rocks/smartshell': 3.2.3
|
||||
tsx: 4.19.2
|
||||
|
||||
@ -4687,7 +4692,7 @@ snapshots:
|
||||
'@push.rocks/smartcache': 1.0.16
|
||||
'@push.rocks/smartenv': 5.0.12
|
||||
'@push.rocks/smartexit': 1.0.23
|
||||
'@push.rocks/smartfile': 11.2.0
|
||||
'@push.rocks/smartfile': 11.2.3
|
||||
'@push.rocks/smartjson': 5.0.20
|
||||
'@push.rocks/smartpath': 5.0.18
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
@ -4725,8 +4730,8 @@ snapshots:
|
||||
dependencies:
|
||||
'@api.global/typedrequest': 3.1.10
|
||||
'@configvault.io/interfaces': 1.0.17
|
||||
'@push.rocks/smartfile': 11.2.0
|
||||
'@push.rocks/smartlog': 3.1.1
|
||||
'@push.rocks/smartfile': 11.2.3
|
||||
'@push.rocks/smartlog': 3.1.8
|
||||
'@push.rocks/smartpath': 5.0.18
|
||||
|
||||
'@push.rocks/smartarchive@3.0.8':
|
||||
@ -4796,7 +4801,7 @@ snapshots:
|
||||
'@push.rocks/smartcli@4.0.11':
|
||||
dependencies:
|
||||
'@push.rocks/lik': 6.2.2
|
||||
'@push.rocks/smartlog': 3.1.1
|
||||
'@push.rocks/smartlog': 3.1.8
|
||||
'@push.rocks/smartobject': 1.0.12
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
'@push.rocks/smartrx': 3.0.10
|
||||
@ -4821,7 +4826,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@push.rocks/lik': 6.2.2
|
||||
'@push.rocks/smartdelay': 3.0.5
|
||||
'@push.rocks/smartlog': 3.1.1
|
||||
'@push.rocks/smartlog': 3.1.8
|
||||
'@push.rocks/smartmongo': 2.0.12(@aws-sdk/credential-providers@3.810.0)(socks@2.8.4)
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
'@push.rocks/smartrx': 3.0.10
|
||||
@ -4857,7 +4862,7 @@ snapshots:
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
tree-kill: 1.2.2
|
||||
|
||||
'@push.rocks/smartexpect@2.4.2':
|
||||
'@push.rocks/smartexpect@2.5.0':
|
||||
dependencies:
|
||||
'@push.rocks/smartdelay': 3.0.5
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
@ -4890,7 +4895,7 @@ snapshots:
|
||||
glob: 10.4.5
|
||||
js-yaml: 4.1.0
|
||||
|
||||
'@push.rocks/smartfile@11.2.0':
|
||||
'@push.rocks/smartfile@11.2.3':
|
||||
dependencies:
|
||||
'@push.rocks/lik': 6.2.2
|
||||
'@push.rocks/smartdelay': 3.0.5
|
||||
@ -4943,13 +4948,13 @@ snapshots:
|
||||
'@api.global/typedrequest-interfaces': 2.0.2
|
||||
'@tsclass/tsclass': 4.4.4
|
||||
|
||||
'@push.rocks/smartlog@3.1.1':
|
||||
'@push.rocks/smartlog@3.1.8':
|
||||
dependencies:
|
||||
'@api.global/typedrequest-interfaces': 3.0.19
|
||||
'@push.rocks/consolecolor': 2.0.2
|
||||
'@push.rocks/isounique': 1.0.5
|
||||
'@push.rocks/smartclickhouse': 2.0.17
|
||||
'@push.rocks/smartfile': 11.2.0
|
||||
'@push.rocks/smartfile': 11.2.3
|
||||
'@push.rocks/smarthash': 3.0.4
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
'@push.rocks/smarttime': 4.1.1
|
||||
@ -5058,7 +5063,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@push.rocks/smartbuffer': 3.0.4
|
||||
'@push.rocks/smartdelay': 3.0.5
|
||||
'@push.rocks/smartfile': 11.2.0
|
||||
'@push.rocks/smartfile': 11.2.3
|
||||
'@push.rocks/smartnetwork': 3.0.2
|
||||
'@push.rocks/smartpath': 5.0.18
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
@ -5111,7 +5116,7 @@ snapshots:
|
||||
'@push.rocks/smarts3@2.2.5':
|
||||
dependencies:
|
||||
'@push.rocks/smartbucket': 3.3.7
|
||||
'@push.rocks/smartfile': 11.2.0
|
||||
'@push.rocks/smartfile': 11.2.3
|
||||
'@push.rocks/smartpath': 5.0.18
|
||||
'@tsclass/tsclass': 4.4.4
|
||||
'@types/s3rver': 3.7.4
|
||||
@ -5148,7 +5153,7 @@ snapshots:
|
||||
'@push.rocks/smartdelay': 3.0.5
|
||||
'@push.rocks/smartenv': 5.0.12
|
||||
'@push.rocks/smartjson': 5.0.20
|
||||
'@push.rocks/smartlog': 3.1.1
|
||||
'@push.rocks/smartlog': 3.1.8
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
'@push.rocks/smartrx': 3.0.10
|
||||
'@push.rocks/smarttime': 4.1.1
|
||||
@ -5246,7 +5251,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@push.rocks/lik': 6.2.2
|
||||
'@push.rocks/smartdelay': 3.0.5
|
||||
'@push.rocks/smartlog': 3.1.1
|
||||
'@push.rocks/smartlog': 3.1.8
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
'@push.rocks/smartrx': 3.0.10
|
||||
'@push.rocks/smarttime': 4.1.1
|
||||
@ -5846,22 +5851,22 @@ snapshots:
|
||||
'@types/body-parser@1.19.5':
|
||||
dependencies:
|
||||
'@types/connect': 3.4.38
|
||||
'@types/node': 22.15.18
|
||||
'@types/node': 22.15.21
|
||||
|
||||
'@types/buffer-json@2.0.3': {}
|
||||
|
||||
'@types/clean-css@4.2.11':
|
||||
dependencies:
|
||||
'@types/node': 22.15.18
|
||||
'@types/node': 22.15.21
|
||||
source-map: 0.6.1
|
||||
|
||||
'@types/connect@3.4.38':
|
||||
dependencies:
|
||||
'@types/node': 22.15.18
|
||||
'@types/node': 22.15.21
|
||||
|
||||
'@types/cors@2.8.18':
|
||||
dependencies:
|
||||
'@types/node': 22.15.18
|
||||
'@types/node': 22.15.21
|
||||
|
||||
'@types/debug@4.1.12':
|
||||
dependencies:
|
||||
@ -5871,14 +5876,14 @@ snapshots:
|
||||
|
||||
'@types/express-serve-static-core@5.0.5':
|
||||
dependencies:
|
||||
'@types/node': 22.15.18
|
||||
'@types/node': 22.15.21
|
||||
'@types/qs': 6.9.18
|
||||
'@types/range-parser': 1.2.7
|
||||
'@types/send': 0.17.4
|
||||
|
||||
'@types/express-serve-static-core@5.0.6':
|
||||
dependencies:
|
||||
'@types/node': 22.15.18
|
||||
'@types/node': 22.15.21
|
||||
'@types/qs': 6.9.18
|
||||
'@types/range-parser': 1.2.7
|
||||
'@types/send': 0.17.4
|
||||
@ -5902,30 +5907,30 @@ snapshots:
|
||||
|
||||
'@types/from2@2.3.5':
|
||||
dependencies:
|
||||
'@types/node': 22.15.18
|
||||
'@types/node': 22.15.21
|
||||
|
||||
'@types/fs-extra@11.0.4':
|
||||
dependencies:
|
||||
'@types/jsonfile': 6.1.4
|
||||
'@types/node': 22.15.18
|
||||
'@types/node': 22.15.21
|
||||
|
||||
'@types/fs-extra@9.0.13':
|
||||
dependencies:
|
||||
'@types/node': 22.15.18
|
||||
'@types/node': 22.15.21
|
||||
|
||||
'@types/glob@7.2.0':
|
||||
dependencies:
|
||||
'@types/minimatch': 5.1.2
|
||||
'@types/node': 22.15.18
|
||||
'@types/node': 22.15.21
|
||||
|
||||
'@types/glob@8.1.0':
|
||||
dependencies:
|
||||
'@types/minimatch': 5.1.2
|
||||
'@types/node': 22.15.18
|
||||
'@types/node': 22.15.21
|
||||
|
||||
'@types/gunzip-maybe@1.4.2':
|
||||
dependencies:
|
||||
'@types/node': 22.15.18
|
||||
'@types/node': 22.15.21
|
||||
|
||||
'@types/hast@3.0.4':
|
||||
dependencies:
|
||||
@ -5947,7 +5952,7 @@ snapshots:
|
||||
|
||||
'@types/jsonfile@6.1.4':
|
||||
dependencies:
|
||||
'@types/node': 22.15.18
|
||||
'@types/node': 22.15.21
|
||||
|
||||
'@types/mdast@4.0.4':
|
||||
dependencies:
|
||||
@ -5965,9 +5970,9 @@ snapshots:
|
||||
|
||||
'@types/node-forge@1.3.11':
|
||||
dependencies:
|
||||
'@types/node': 22.15.18
|
||||
'@types/node': 22.15.21
|
||||
|
||||
'@types/node@22.15.18':
|
||||
'@types/node@22.15.21':
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
|
||||
@ -5983,30 +5988,30 @@ snapshots:
|
||||
|
||||
'@types/s3rver@3.7.4':
|
||||
dependencies:
|
||||
'@types/node': 22.15.18
|
||||
'@types/node': 22.15.21
|
||||
|
||||
'@types/semver@7.7.0': {}
|
||||
|
||||
'@types/send@0.17.4':
|
||||
dependencies:
|
||||
'@types/mime': 1.3.5
|
||||
'@types/node': 22.15.18
|
||||
'@types/node': 22.15.21
|
||||
|
||||
'@types/serve-static@1.15.7':
|
||||
dependencies:
|
||||
'@types/http-errors': 2.0.4
|
||||
'@types/node': 22.15.18
|
||||
'@types/node': 22.15.21
|
||||
'@types/send': 0.17.4
|
||||
|
||||
'@types/symbol-tree@3.2.5': {}
|
||||
|
||||
'@types/tar-stream@2.2.3':
|
||||
dependencies:
|
||||
'@types/node': 22.15.18
|
||||
'@types/node': 22.15.21
|
||||
|
||||
'@types/through2@2.0.41':
|
||||
dependencies:
|
||||
'@types/node': 22.15.18
|
||||
'@types/node': 22.15.21
|
||||
|
||||
'@types/triple-beam@1.3.5': {}
|
||||
|
||||
@ -6030,7 +6035,7 @@ snapshots:
|
||||
|
||||
'@types/whatwg-url@8.2.2':
|
||||
dependencies:
|
||||
'@types/node': 22.15.18
|
||||
'@types/node': 22.15.21
|
||||
'@types/webidl-conversions': 7.0.3
|
||||
|
||||
'@types/which@2.0.2': {}
|
||||
@ -6039,11 +6044,11 @@ snapshots:
|
||||
|
||||
'@types/ws@8.18.1':
|
||||
dependencies:
|
||||
'@types/node': 22.15.18
|
||||
'@types/node': 22.15.21
|
||||
|
||||
'@types/yauzl@2.10.3':
|
||||
dependencies:
|
||||
'@types/node': 22.15.18
|
||||
'@types/node': 22.15.21
|
||||
optional: true
|
||||
|
||||
'@ungap/structured-clone@1.3.0': {}
|
||||
@ -6502,7 +6507,7 @@ snapshots:
|
||||
engine.io@6.6.4:
|
||||
dependencies:
|
||||
'@types/cors': 2.8.18
|
||||
'@types/node': 22.15.18
|
||||
'@types/node': 22.15.21
|
||||
accepts: 1.3.8
|
||||
base64id: 2.0.0
|
||||
cookie: 0.7.2
|
||||
@ -6852,7 +6857,7 @@ snapshots:
|
||||
glob@11.0.2:
|
||||
dependencies:
|
||||
foreground-child: 3.3.1
|
||||
jackspeak: 4.1.0
|
||||
jackspeak: 4.1.1
|
||||
minimatch: 10.0.1
|
||||
minipass: 7.1.2
|
||||
package-json-from-dist: 1.0.1
|
||||
@ -7104,7 +7109,7 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@pkgjs/parseargs': 0.11.0
|
||||
|
||||
jackspeak@4.1.0:
|
||||
jackspeak@4.1.1:
|
||||
dependencies:
|
||||
'@isaacs/cliui': 8.0.2
|
||||
|
||||
@ -8494,6 +8499,8 @@ snapshots:
|
||||
|
||||
typescript@5.7.3: {}
|
||||
|
||||
typescript@5.8.3: {}
|
||||
|
||||
uglify-js@3.19.3: {}
|
||||
|
||||
uint8array-extras@1.4.0: {}
|
||||
|
@ -12,7 +12,7 @@ This project integrates tstest with tapbundle through a modular architecture:
|
||||
|
||||
### Test Execution Flow
|
||||
|
||||
1. **CLI Entry Point** (`cli.js` <20> `cli.ts.js` <20> `cli.child.ts`)
|
||||
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`
|
||||
@ -40,9 +40,17 @@ This project integrates tstest with tapbundle through a modular architecture:
|
||||
- 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
|
||||
- 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/`
|
||||
- Compilation order is resolved automatically based on dependencies in tspublish.json files
|
||||
- Protocol imports use compiled dist directories:
|
||||
```typescript
|
||||
// In ts/tstest.classes.tap.parser.ts
|
||||
import { ProtocolParser } from '../dist_ts_tapbundle_protocol/index.js';
|
||||
|
||||
// In ts_tapbundle/tapbundle.classes.tap.ts
|
||||
import { ProtocolEmitter } from '../dist_ts_tapbundle_protocol/index.js';
|
||||
```
|
||||
|
||||
### Test Scripts
|
||||
|
||||
@ -59,4 +67,62 @@ The framework automatically detects the runtime environment:
|
||||
- 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.
|
||||
This architecture allows for seamless testing across both Node.js and browser environments while maintaining a clean separation of concerns.
|
||||
|
||||
## Logging System
|
||||
|
||||
### Log File Naming (Fixed in v1.9.1)
|
||||
|
||||
When using the `--logfile` flag, tstest creates log files in `.nogit/testlogs/`. The log file naming was updated to preserve directory structure and prevent collisions:
|
||||
|
||||
- **Old behavior**: `test/tapbundle/test.ts` → `.nogit/testlogs/test.log`
|
||||
- **New behavior**: `test/tapbundle/test.ts` → `.nogit/testlogs/test__tapbundle__test.log`
|
||||
|
||||
This fix ensures that test files with the same basename in different directories don't overwrite each other's logs. The implementation:
|
||||
1. Takes the relative path from the current working directory
|
||||
2. Replaces path separators (`/`) with double underscores (`__`)
|
||||
3. Removes the `.ts` extension
|
||||
4. Creates a flat filename that preserves the directory structure
|
||||
|
||||
### Test Timing Display (Fixed in v1.9.2)
|
||||
|
||||
Fixed an issue where test timing was displayed incorrectly with duplicate values like:
|
||||
- Before: `✅ test name # time=133ms (0ms)`
|
||||
- After: `✅ test name (133ms)`
|
||||
|
||||
The issue was in the TAP parser regex which was greedily capturing the entire line including the TAP timing comment. Changed the regex from `(.*)` to `(.*?)` to make it non-greedy, properly separating the test name from the timing metadata.
|
||||
|
||||
## Protocol Limitations and Improvements
|
||||
|
||||
### Current TAP Protocol Issues
|
||||
The current implementation uses standard TAP format with metadata in comments:
|
||||
```
|
||||
ok 1 - test name # time=123ms
|
||||
```
|
||||
|
||||
This has several limitations:
|
||||
1. **Delimiter Conflict**: Test descriptions containing `#` can break parsing
|
||||
2. **Regex Fragility**: Complex regex patterns that are hard to maintain
|
||||
3. **Limited Metadata**: Difficult to add rich error information or custom data
|
||||
|
||||
### Planned Protocol V2
|
||||
A new internal protocol is being designed that will:
|
||||
- Use Unicode delimiters `⟦TSTEST:⟧` that won't conflict with test content
|
||||
- Support structured JSON metadata
|
||||
- Allow rich error reporting with stack traces and diffs
|
||||
- Completely replace v1 protocol (no backwards compatibility)
|
||||
|
||||
### ts_tapbundle_protocol Directory
|
||||
The protocol v2 implementation is contained in a separate `ts_tapbundle_protocol` directory:
|
||||
- **Isomorphic Code**: All protocol code works in both browser and Node.js environments
|
||||
- **No Platform Dependencies**: No Node.js-specific imports, ensuring true cross-platform compatibility
|
||||
- **Clean Separation**: Protocol logic is isolated from platform-specific code in tstest and tapbundle
|
||||
- **Shared Implementation**: Both tstest (parser) and tapbundle (emitter) use the same protocol classes
|
||||
- **Build Process**:
|
||||
- Compiled by `pnpm build` via tsbuild to `dist_ts_tapbundle_protocol/`
|
||||
- Build order managed through tspublish.json files
|
||||
- Other modules import from the compiled dist directory, not source
|
||||
|
||||
This architectural decision ensures the protocol can be used in any JavaScript environment without modification and maintains proper build dependencies.
|
||||
|
||||
See `readme.protocol.md` for the full specification and `ts_tapbundle_protocol/` for the implementation.
|
610
readme.md
610
readme.md
@ -19,6 +19,14 @@
|
||||
- 📝 **Detailed Logging** - Optional file logging for debugging
|
||||
- ⚡ **Performance Metrics** - See which tests are slow
|
||||
- 🤖 **CI/CD Ready** - JSON output mode for automation
|
||||
- 🏷️ **Tag-based Filtering** - Run only tests with specific tags
|
||||
- 🎯 **Parallel Test Execution** - Run tests in parallel groups
|
||||
- 🔧 **Test Lifecycle Hooks** - beforeEach/afterEach support
|
||||
- 📸 **Snapshot Testing** - Compare test outputs with saved snapshots
|
||||
- ⏳ **Timeout Control** - Set custom timeouts for tests
|
||||
- 🔁 **Retry Logic** - Automatically retry failing tests
|
||||
- 🛠️ **Test Fixtures** - Create reusable test data
|
||||
- 📦 **Browser-Compatible** - Full browser support with embedded tapbundle
|
||||
|
||||
## Installation
|
||||
|
||||
@ -60,7 +68,11 @@ tstest "test/unit/*.ts"
|
||||
| `--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` |
|
||||
| `--logfile` | Save detailed logs with automatic error and diff tracking |
|
||||
| `--tags <tags>` | Run only tests with specific tags (comma-separated) |
|
||||
| `--timeout <seconds>` | Timeout test files after specified seconds |
|
||||
| `--startFrom <n>` | Start running from test file number n |
|
||||
| `--stopAt <n>` | Stop running at test file number n |
|
||||
|
||||
### Example Outputs
|
||||
|
||||
@ -132,12 +144,12 @@ tstest supports different test environments through file naming:
|
||||
| `*.browser.ts` | Browser environment | `test.ui.browser.ts` |
|
||||
| `*.both.ts` | Both Node.js and browser | `test.isomorphic.both.ts` |
|
||||
|
||||
### Writing Tests
|
||||
### Writing Tests with tapbundle
|
||||
|
||||
tstest uses TAP (Test Anything Protocol) for test output. Use `@pushrocks/tapbundle` for the best experience:
|
||||
tstest includes tapbundle, a powerful TAP-based test framework. Import it from the embedded tapbundle:
|
||||
|
||||
```typescript
|
||||
import { expect, tap } from '@push.rocks/tapbundle';
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
|
||||
tap.test('my awesome test', async () => {
|
||||
const result = await myFunction();
|
||||
@ -147,6 +159,403 @@ tap.test('my awesome test', async () => {
|
||||
tap.start();
|
||||
```
|
||||
|
||||
**Module Exports**
|
||||
|
||||
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
|
||||
|
||||
## tapbundle Test Framework
|
||||
|
||||
### Basic Test Syntax
|
||||
|
||||
```typescript
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
|
||||
// Basic test
|
||||
tap.test('should perform basic arithmetic', async () => {
|
||||
expect(2 + 2).toEqual(4);
|
||||
});
|
||||
|
||||
// Async test with tools
|
||||
tap.test('async operations', async (tools) => {
|
||||
await tools.delayFor(100); // delay for 100ms
|
||||
const result = await fetchData();
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
|
||||
// Start test execution
|
||||
tap.start();
|
||||
```
|
||||
|
||||
### Test Modifiers and Chaining
|
||||
|
||||
```typescript
|
||||
// Skip a test
|
||||
tap.skip.test('not ready yet', async () => {
|
||||
// This test will be skipped
|
||||
});
|
||||
|
||||
// Run only this test (exclusive)
|
||||
tap.only.test('focus on this', async () => {
|
||||
// Only this test will run
|
||||
});
|
||||
|
||||
// Todo test
|
||||
tap.todo('implement later', async () => {
|
||||
// Marked as todo
|
||||
});
|
||||
|
||||
// Chaining modifiers
|
||||
tap.timeout(5000)
|
||||
.retry(3)
|
||||
.tags('api', 'integration')
|
||||
.test('complex test', async (tools) => {
|
||||
// Test with 5s timeout, 3 retries, and tags
|
||||
});
|
||||
```
|
||||
|
||||
### Test Organization with describe()
|
||||
|
||||
```typescript
|
||||
tap.describe('User Management', () => {
|
||||
let testDatabase;
|
||||
|
||||
tap.beforeEach(async () => {
|
||||
testDatabase = await createTestDB();
|
||||
});
|
||||
|
||||
tap.afterEach(async () => {
|
||||
await testDatabase.cleanup();
|
||||
});
|
||||
|
||||
tap.test('should create user', async () => {
|
||||
const user = await testDatabase.createUser({ name: 'John' });
|
||||
expect(user.id).toBeDefined();
|
||||
});
|
||||
|
||||
tap.describe('User Permissions', () => {
|
||||
tap.test('should set admin role', async () => {
|
||||
// Nested describe blocks
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Test Tools (Available in Test Function)
|
||||
|
||||
Every test function receives a `tools` parameter with utilities:
|
||||
|
||||
```typescript
|
||||
tap.test('using test tools', async (tools) => {
|
||||
// Delay utilities
|
||||
await tools.delayFor(1000); // delay for 1000ms
|
||||
await tools.delayForRandom(100, 500); // random delay between 100-500ms
|
||||
|
||||
// Skip test conditionally
|
||||
tools.skipIf(process.env.CI === 'true', 'Skipping in CI');
|
||||
|
||||
// Skip test unconditionally
|
||||
if (!apiKeyAvailable) {
|
||||
tools.skip('API key not available');
|
||||
}
|
||||
|
||||
// Mark as todo
|
||||
tools.todo('Needs implementation');
|
||||
|
||||
// Retry configuration
|
||||
tools.retry(3); // Set retry count
|
||||
|
||||
// Timeout configuration
|
||||
tools.timeout(10000); // Set timeout to 10s
|
||||
|
||||
// Context sharing between tests
|
||||
tools.context.set('userId', 12345);
|
||||
const userId = tools.context.get('userId');
|
||||
|
||||
// Deferred promises
|
||||
const deferred = tools.defer();
|
||||
setTimeout(() => deferred.resolve('done'), 100);
|
||||
await deferred.promise;
|
||||
|
||||
// Colored console output
|
||||
const coloredString = await tools.coloredString('Success!', 'green');
|
||||
console.log(coloredString);
|
||||
|
||||
// Error handling helper
|
||||
const error = await tools.returnError(async () => {
|
||||
throw new Error('Expected error');
|
||||
});
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
});
|
||||
```
|
||||
|
||||
### Snapshot Testing
|
||||
|
||||
```typescript
|
||||
tap.test('snapshot test', async (tools) => {
|
||||
const output = generateComplexOutput();
|
||||
|
||||
// Compare with saved snapshot
|
||||
await tools.matchSnapshot(output);
|
||||
|
||||
// Named snapshots for multiple checks in one test
|
||||
await tools.matchSnapshot(output.header, 'header');
|
||||
await tools.matchSnapshot(output.body, 'body');
|
||||
});
|
||||
|
||||
// Update snapshots with: UPDATE_SNAPSHOTS=true tstest test/
|
||||
```
|
||||
|
||||
### Test Fixtures
|
||||
|
||||
```typescript
|
||||
// Define reusable fixtures
|
||||
tap.defineFixture('testUser', async (data) => ({
|
||||
id: Date.now(),
|
||||
name: data?.name || 'Test User',
|
||||
email: data?.email || 'test@example.com',
|
||||
created: new Date()
|
||||
}));
|
||||
|
||||
tap.defineFixture('testPost', async (data) => ({
|
||||
id: Date.now(),
|
||||
title: data?.title || 'Test Post',
|
||||
authorId: data?.authorId || 1
|
||||
}));
|
||||
|
||||
// Use fixtures in tests
|
||||
tap.test('fixture test', async (tools) => {
|
||||
const user = await tools.fixture('testUser', { name: 'John' });
|
||||
const post = await tools.fixture('testPost', { authorId: user.id });
|
||||
|
||||
expect(post.authorId).toEqual(user.id);
|
||||
|
||||
// Factory pattern for multiple instances
|
||||
const users = await tools.factory('testUser').createMany(5);
|
||||
expect(users).toHaveLength(5);
|
||||
});
|
||||
```
|
||||
|
||||
### Parallel Test Execution
|
||||
|
||||
```typescript
|
||||
// Parallel tests within a file
|
||||
tap.testParallel('parallel test 1', async () => {
|
||||
await heavyOperation();
|
||||
});
|
||||
|
||||
tap.testParallel('parallel test 2', async () => {
|
||||
await anotherHeavyOperation();
|
||||
});
|
||||
|
||||
// File naming for parallel groups
|
||||
// test.api.para__1.ts - runs in parallel with other para__1 files
|
||||
// test.db.para__1.ts - runs in parallel with other para__1 files
|
||||
// test.auth.para__2.ts - runs after para__1 group completes
|
||||
```
|
||||
|
||||
### Assertions with expect()
|
||||
|
||||
tapbundle uses @push.rocks/smartexpect for assertions:
|
||||
|
||||
```typescript
|
||||
// Basic assertions
|
||||
expect(value).toEqual(5);
|
||||
expect(value).not.toEqual(10);
|
||||
expect(obj).toDeepEqual({ a: 1, b: 2 });
|
||||
|
||||
// Type assertions
|
||||
expect('hello').toBeTypeofString();
|
||||
expect(42).toBeTypeofNumber();
|
||||
expect(true).toBeTypeofBoolean();
|
||||
expect([]).toBeArray();
|
||||
expect({}).toBeTypeOf('object');
|
||||
|
||||
// Comparison assertions
|
||||
expect(5).toBeGreaterThan(3);
|
||||
expect(3).toBeLessThan(5);
|
||||
expect(5).toBeGreaterThanOrEqual(5);
|
||||
expect(5).toBeLessThanOrEqual(5);
|
||||
expect(0.1 + 0.2).toBeCloseTo(0.3, 10);
|
||||
|
||||
// Truthiness
|
||||
expect(true).toBeTrue();
|
||||
expect(false).toBeFalse();
|
||||
expect('text').toBeTruthy();
|
||||
expect(0).toBeFalsy();
|
||||
expect(null).toBeNull();
|
||||
expect(undefined).toBeUndefined();
|
||||
expect(null).toBeNullOrUndefined();
|
||||
|
||||
// String assertions
|
||||
expect('hello world').toStartWith('hello');
|
||||
expect('hello world').toEndWith('world');
|
||||
expect('hello world').toInclude('lo wo');
|
||||
expect('hello world').toMatch(/^hello/);
|
||||
expect('option').toBeOneOf(['choice', 'option', 'alternative']);
|
||||
|
||||
// Array assertions
|
||||
expect([1, 2, 3]).toContain(2);
|
||||
expect([1, 2, 3]).toContainAll([1, 3]);
|
||||
expect([1, 2, 3]).toExclude(4);
|
||||
expect([1, 2, 3]).toHaveLength(3);
|
||||
expect([]).toBeEmptyArray();
|
||||
expect([{ id: 1 }]).toContainEqual({ id: 1 });
|
||||
|
||||
// Object assertions
|
||||
expect(obj).toHaveProperty('name');
|
||||
expect(obj).toHaveProperty('user.email', 'test@example.com');
|
||||
expect(obj).toHaveDeepProperty(['level1', 'level2']);
|
||||
expect(obj).toMatchObject({ name: 'John' });
|
||||
|
||||
// Function assertions
|
||||
expect(() => { throw new Error('test'); }).toThrow();
|
||||
expect(() => { throw new Error('test'); }).toThrow(Error);
|
||||
expect(() => { throw new Error('test error'); }).toThrowErrorMatching(/test/);
|
||||
expect(myFunction).not.toThrow();
|
||||
|
||||
// Promise assertions
|
||||
await expect(Promise.resolve('value')).resolves.toEqual('value');
|
||||
await expect(Promise.reject(new Error('fail'))).rejects.toThrow();
|
||||
|
||||
// Custom assertions
|
||||
expect(7).customAssertion(
|
||||
value => value % 2 === 1,
|
||||
'Value is not odd'
|
||||
);
|
||||
```
|
||||
|
||||
### Pre-tasks
|
||||
|
||||
Run setup tasks before tests start:
|
||||
|
||||
```typescript
|
||||
tap.preTask('setup database', async () => {
|
||||
await initializeTestDatabase();
|
||||
console.log('Database initialized');
|
||||
});
|
||||
|
||||
tap.preTask('load environment', async () => {
|
||||
await loadTestEnvironment();
|
||||
});
|
||||
|
||||
// Pre-tasks run in order before any tests
|
||||
```
|
||||
|
||||
### Tag-based Test Filtering
|
||||
|
||||
```typescript
|
||||
// Tag individual tests
|
||||
tap.tags('unit', 'api')
|
||||
.test('api unit test', async () => {
|
||||
// Test code
|
||||
});
|
||||
|
||||
tap.tags('integration', 'slow')
|
||||
.test('database integration', async () => {
|
||||
// Test code
|
||||
});
|
||||
|
||||
// Run only tests with specific tags
|
||||
// tstest test/ --tags unit,api
|
||||
```
|
||||
|
||||
### Context Sharing
|
||||
|
||||
Share data between tests:
|
||||
|
||||
```typescript
|
||||
tap.test('first test', async (tools) => {
|
||||
const sessionId = await createSession();
|
||||
tools.context.set('sessionId', sessionId);
|
||||
});
|
||||
|
||||
tap.test('second test', async (tools) => {
|
||||
const sessionId = tools.context.get('sessionId');
|
||||
expect(sessionId).toBeDefined();
|
||||
|
||||
// Cleanup
|
||||
tools.context.delete('sessionId');
|
||||
});
|
||||
```
|
||||
|
||||
### Browser Testing with webhelpers
|
||||
|
||||
For browser-specific tests:
|
||||
|
||||
```typescript
|
||||
import { tap, webhelpers } from '@git.zone/tstest/tapbundle';
|
||||
|
||||
tap.test('DOM manipulation', async () => {
|
||||
// Create DOM elements from HTML strings
|
||||
const element = await webhelpers.fixture(webhelpers.html`
|
||||
<div class="test-container">
|
||||
<h1>Test Title</h1>
|
||||
<button id="test-btn">Click Me</button>
|
||||
</div>
|
||||
`);
|
||||
|
||||
expect(element.querySelector('h1').textContent).toEqual('Test Title');
|
||||
|
||||
// Simulate interactions
|
||||
const button = element.querySelector('#test-btn');
|
||||
button.click();
|
||||
});
|
||||
|
||||
tap.test('CSS testing', async () => {
|
||||
const styles = webhelpers.css`
|
||||
.test-class {
|
||||
color: red;
|
||||
font-size: 16px;
|
||||
}
|
||||
`;
|
||||
|
||||
// styles is a string that can be injected into the page
|
||||
expect(styles).toInclude('color: red');
|
||||
});
|
||||
```
|
||||
|
||||
### Advanced Error Handling
|
||||
|
||||
```typescript
|
||||
tap.test('error handling', async (tools) => {
|
||||
// Capture errors without failing the test
|
||||
const error = await tools.returnError(async () => {
|
||||
await functionThatThrows();
|
||||
});
|
||||
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect(error.message).toEqual('Expected error message');
|
||||
});
|
||||
```
|
||||
|
||||
### Test Wrap
|
||||
|
||||
Create wrapped test environments:
|
||||
|
||||
```typescript
|
||||
import { TapWrap } from '@git.zone/tstest/tapbundle';
|
||||
|
||||
const tapWrap = new TapWrap({
|
||||
before: async () => {
|
||||
console.log('Before all tests');
|
||||
await globalSetup();
|
||||
},
|
||||
after: async () => {
|
||||
console.log('After all tests');
|
||||
await globalCleanup();
|
||||
}
|
||||
});
|
||||
|
||||
// Tests registered here will have the wrap lifecycle
|
||||
tapWrap.tap.test('wrapped test', async () => {
|
||||
// This test runs with the wrap setup/teardown
|
||||
});
|
||||
```
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Glob Pattern Support
|
||||
@ -163,14 +572,90 @@ tstest "test/integration/*.test.ts"
|
||||
tstest "test/**/*.spec.ts" "test/**/*.test.ts"
|
||||
```
|
||||
|
||||
### Automatic Logging
|
||||
**Important**: Always quote glob patterns to prevent shell expansion. Without quotes, the shell will expand the pattern and only pass the first matching file to tstest.
|
||||
|
||||
### Enhanced Test Logging
|
||||
|
||||
The `--logfile` option provides intelligent test logging with automatic organization:
|
||||
|
||||
Use `--logfile` to automatically save test output:
|
||||
```bash
|
||||
tstest test/ --logfile
|
||||
```
|
||||
|
||||
This creates detailed logs in `.nogit/testlogs/[testname].log` for each test file.
|
||||
**Log Organization:**
|
||||
- **Current Run**: `.nogit/testlogs/[testname].log`
|
||||
- **Previous Run**: `.nogit/testlogs/previous/[testname].log`
|
||||
- **Failed Tests**: `.nogit/testlogs/00err/[testname].log`
|
||||
- **Changed Output**: `.nogit/testlogs/00diff/[testname].log`
|
||||
|
||||
**Features:**
|
||||
- Previous logs are automatically moved to the `previous/` folder
|
||||
- Failed tests create copies in `00err/` for quick identification
|
||||
- Tests with changed output create diff reports in `00diff/`
|
||||
- The `00err/` and `00diff/` folders are cleared on each run
|
||||
|
||||
**Example Diff Report:**
|
||||
```
|
||||
DIFF REPORT: test__api__integration.log
|
||||
Generated: 2025-05-24T01:29:13.847Z
|
||||
================================================================================
|
||||
|
||||
- [Line 8] ✅ api test passes (150ms)
|
||||
+ [Line 8] ✅ api test passes (165ms)
|
||||
|
||||
================================================================================
|
||||
Previous version had 40 lines
|
||||
Current version has 40 lines
|
||||
```
|
||||
|
||||
### Test Timeout Protection
|
||||
|
||||
Prevent runaway tests with the `--timeout` option:
|
||||
|
||||
```bash
|
||||
# Timeout any test file that runs longer than 60 seconds
|
||||
tstest test/ --timeout 60
|
||||
|
||||
# Shorter timeout for unit tests
|
||||
tstest test/unit/ --timeout 10
|
||||
```
|
||||
|
||||
When a test exceeds the timeout:
|
||||
- The test process is terminated (SIGTERM)
|
||||
- The test is marked as failed
|
||||
- An error log is created in `.nogit/testlogs/00err/`
|
||||
- Clear error message shows the timeout duration
|
||||
|
||||
### Test File Range Control
|
||||
|
||||
Run specific ranges of test files using `--startFrom` and `--stopAt`:
|
||||
|
||||
```bash
|
||||
# Run tests starting from the 5th file
|
||||
tstest test/ --startFrom 5
|
||||
|
||||
# Run only files 5 through 10
|
||||
tstest test/ --startFrom 5 --stopAt 10
|
||||
|
||||
# Run only the first 3 test files
|
||||
tstest test/ --stopAt 3
|
||||
```
|
||||
|
||||
This is particularly useful for:
|
||||
- Debugging specific test failures in large test suites
|
||||
- Running tests in chunks on different CI runners
|
||||
- Quickly testing changes to specific test files
|
||||
|
||||
The output shows which files are skipped:
|
||||
```
|
||||
⏭️ test/auth.test.ts (1/10)
|
||||
Skipped: before start range (5)
|
||||
⏭️ test/user.test.ts (2/10)
|
||||
Skipped: before start range (5)
|
||||
▶️ test/api.test.ts (5/10)
|
||||
Runtime: node.js
|
||||
✅ api endpoints work (145ms)
|
||||
```
|
||||
|
||||
### Performance Analysis
|
||||
|
||||
@ -181,6 +666,26 @@ In verbose mode, see performance metrics:
|
||||
Slowest test: api integration test (486ms)
|
||||
```
|
||||
|
||||
### Parallel Test Groups
|
||||
|
||||
Tests can be organized into parallel groups for concurrent execution:
|
||||
|
||||
```
|
||||
━━━ Parallel Group: para__1 ━━━
|
||||
▶️ test/auth.para__1.ts
|
||||
▶️ test/user.para__1.ts
|
||||
... tests run concurrently ...
|
||||
──────────────────────────────────
|
||||
|
||||
━━━ Parallel Group: para__2 ━━━
|
||||
▶️ test/db.para__2.ts
|
||||
▶️ test/api.para__2.ts
|
||||
... tests run concurrently ...
|
||||
──────────────────────────────────
|
||||
```
|
||||
|
||||
Files with the same parallel group suffix (e.g., `para__1`) run simultaneously, while different groups run sequentially.
|
||||
|
||||
### CI/CD Integration
|
||||
|
||||
For continuous integration, combine quiet and JSON modes:
|
||||
@ -192,13 +697,92 @@ tstest test/ --json > test-results.json
|
||||
tstest test/ --quiet
|
||||
```
|
||||
|
||||
## Contribution
|
||||
**Advanced CI Example:**
|
||||
```bash
|
||||
# Run tests with comprehensive logging and safety features
|
||||
tstest test/ \
|
||||
--timeout 300 \
|
||||
--logfile \
|
||||
--json > test-results.json
|
||||
|
||||
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). :)
|
||||
# Run specific test chunks in parallel CI jobs
|
||||
tstest test/ --startFrom 1 --stopAt 10 # Job 1
|
||||
tstest test/ --startFrom 11 --stopAt 20 # Job 2
|
||||
tstest test/ --startFrom 21 # Job 3
|
||||
```
|
||||
|
||||
## License
|
||||
### Debugging Failed Tests
|
||||
|
||||
> MIT licensed | **©** [Lossless GmbH](https://lossless.gmbh)
|
||||
| By using this npm module you agree to our [privacy policy](https://lossless.gmbH/privacy)
|
||||
When tests fail, use the enhanced logging features:
|
||||
|
||||
[](https://maintainedby.lossless.com)
|
||||
```bash
|
||||
# Run with logging to capture detailed output
|
||||
tstest test/ --logfile --verbose
|
||||
|
||||
# Check error logs
|
||||
ls .nogit/testlogs/00err/
|
||||
|
||||
# Review diffs for flaky tests
|
||||
cat .nogit/testlogs/00diff/test__api__endpoints.log
|
||||
|
||||
# Re-run specific failed tests
|
||||
tstest test/api/endpoints.test.ts --verbose --timeout 60
|
||||
```
|
||||
|
||||
## Changelog
|
||||
|
||||
### Version 1.10.0
|
||||
- ⏱️ Added `--timeout <seconds>` option for test file timeout protection
|
||||
- 🎯 Added `--startFrom <n>` and `--stopAt <n>` options for test file range control
|
||||
- 📁 Enhanced `--logfile` with intelligent log organization:
|
||||
- Previous logs moved to `previous/` folder
|
||||
- Failed tests copied to `00err/` folder
|
||||
- Changed tests create diff reports in `00diff/` folder
|
||||
- 🔍 Improved test discovery to show skipped files with clear reasons
|
||||
- 🐛 Fixed TypeScript compilation warnings and unused variables
|
||||
- 📊 Test summaries now include skipped file counts
|
||||
|
||||
### Version 1.9.2
|
||||
- 🐛 Fixed test timing display issue (removed duplicate timing in output)
|
||||
- 📝 Improved internal protocol design documentation
|
||||
- 🔧 Added protocol v2 utilities for future improvements
|
||||
|
||||
### Version 1.9.1
|
||||
- 🐛 Fixed log file naming to preserve directory structure
|
||||
- 📁 Log files now prevent collisions: `test__dir__file.log`
|
||||
|
||||
### Version 1.9.0
|
||||
- 📚 Comprehensive documentation update
|
||||
- 🏗️ Embedded tapbundle for better integration
|
||||
- 🌐 Full browser compatibility
|
||||
|
||||
### Version 1.8.0
|
||||
- 📦 Embedded tapbundle directly into tstest project
|
||||
- 🌐 Made tapbundle fully browser-compatible
|
||||
- 📸 Added snapshot testing with base64-encoded communication protocol
|
||||
- 🏷️ Introduced tag-based test filtering
|
||||
- 🔧 Enhanced test lifecycle hooks (beforeEach/afterEach)
|
||||
- 🎯 Fixed parallel test execution and grouping
|
||||
- ⏳ Improved timeout and retry mechanisms
|
||||
- 🛠️ Added test fixtures for reusable test data
|
||||
- 📊 Enhanced TAP parser for better test reporting
|
||||
- 🐛 Fixed glob pattern handling in shell scripts
|
||||
|
||||
## License and Legal Information
|
||||
|
||||
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license.md) file within this repository.
|
||||
|
||||
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||
|
||||
### Trademarks
|
||||
|
||||
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.
|
||||
|
||||
### Company Information
|
||||
|
||||
Task Venture Capital GmbH
|
||||
Registered at District court Bremen HRB 35230 HB, Germany
|
||||
|
||||
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
|
||||
|
||||
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
263
readme.plan.md
263
readme.plan.md
@ -2,6 +2,96 @@
|
||||
|
||||
!! FIRST: Reread /home/philkunz/.claude/CLAUDE.md to ensure following all guidelines !!
|
||||
|
||||
## Improved Internal Protocol (NEW - Critical)
|
||||
|
||||
### Current Issues
|
||||
- TAP protocol uses `#` for metadata which conflicts with test descriptions containing `#`
|
||||
- Fragile regex parsing that breaks with special characters
|
||||
- Limited extensibility for new metadata types
|
||||
|
||||
### Proposed Solution: Protocol V2
|
||||
- Use Unicode delimiters `⟦TSTEST:META:{}⟧` that won't appear in test names
|
||||
- Structured JSON metadata format
|
||||
- Separate protocol blocks for complex data (errors, snapshots)
|
||||
- Complete replacement of v1 (no backwards compatibility needed)
|
||||
|
||||
### Implementation
|
||||
- Phase 1: Create protocol v2 implementation in ts_tapbundle_protocol
|
||||
- Phase 2: Replace all v1 code in both tstest and tapbundle with v2
|
||||
- Phase 3: Delete all v1 parsing and generation code
|
||||
|
||||
#### ts_tapbundle_protocol Directory
|
||||
The protocol v2 implementation will be contained in the `ts_tapbundle_protocol` directory as isomorphic TypeScript code:
|
||||
- **Isomorphic Design**: All code must work in both browser and Node.js environments
|
||||
- **No Node.js Imports**: No Node.js-specific modules allowed (no fs, path, child_process, etc.)
|
||||
- **Protocol Classes**: Contains classes implementing all sides of the protocol:
|
||||
- `ProtocolEmitter`: For generating protocol v2 messages (used by tapbundle)
|
||||
- `ProtocolParser`: For parsing protocol v2 messages (used by tstest)
|
||||
- `ProtocolMessage`: Base classes for different message types
|
||||
- `ProtocolTypes`: TypeScript interfaces and types for protocol structures
|
||||
- **Pure TypeScript**: Only browser-compatible APIs and pure TypeScript/JavaScript code
|
||||
- **Build Integration**:
|
||||
- Compiled by `pnpm build` (via tsbuild) to `dist_ts_tapbundle_protocol/`
|
||||
- Build order defined in tspublish.json files
|
||||
- Imported by ts and ts_tapbundle modules from the compiled dist directory
|
||||
|
||||
See `readme.protocol.md` for detailed specification.
|
||||
|
||||
## Test Configuration System (NEW)
|
||||
|
||||
### Global Test Configuration via 00init.ts
|
||||
- **Discovery**: Check for `test/00init.ts` before running tests
|
||||
- **Execution**: Import and execute before any test files if found
|
||||
- **Purpose**: Define project-wide default test settings
|
||||
|
||||
### tap.settings() API
|
||||
```typescript
|
||||
interface TapSettings {
|
||||
// Timing
|
||||
timeout?: number; // Default timeout for all tests (ms)
|
||||
slowThreshold?: number; // Mark tests as slow if they exceed this (ms)
|
||||
|
||||
// Execution Control
|
||||
bail?: boolean; // Stop on first test failure
|
||||
retries?: number; // Number of retries for failed tests
|
||||
retryDelay?: number; // Delay between retries (ms)
|
||||
|
||||
// Output Control
|
||||
suppressConsole?: boolean; // Suppress console output in passing tests
|
||||
verboseErrors?: boolean; // Show full stack traces
|
||||
showTestDuration?: boolean; // Show duration for each test
|
||||
|
||||
// Parallel Execution
|
||||
maxConcurrency?: number; // Max parallel tests (for .para files)
|
||||
isolateTests?: boolean; // Run each test in fresh context
|
||||
|
||||
// Lifecycle Hooks
|
||||
beforeAll?: () => Promise<void> | void;
|
||||
afterAll?: () => Promise<void> | void;
|
||||
beforeEach?: (testName: string) => Promise<void> | void;
|
||||
afterEach?: (testName: string, passed: boolean) => Promise<void> | void;
|
||||
|
||||
// Environment
|
||||
env?: Record<string, string>; // Additional environment variables
|
||||
|
||||
// Features
|
||||
enableSnapshots?: boolean; // Enable snapshot testing
|
||||
snapshotDirectory?: string; // Custom snapshot directory
|
||||
updateSnapshots?: boolean; // Update snapshots instead of comparing
|
||||
}
|
||||
```
|
||||
|
||||
### Settings Inheritance
|
||||
- Global (00init.ts) → File level → Test level
|
||||
- More specific settings override less specific ones
|
||||
- Arrays/objects are merged, primitives are replaced
|
||||
|
||||
### Implementation Phases
|
||||
1. **Core Infrastructure**: Settings storage and merge logic
|
||||
2. **Discovery**: 00init.ts loading mechanism
|
||||
3. **Application**: Apply settings to test execution
|
||||
4. **Advanced**: Parallel execution and snapshot configuration
|
||||
|
||||
## 1. Enhanced Communication Between tapbundle and tstest
|
||||
|
||||
### 1.1 Real-time Test Progress API
|
||||
@ -18,51 +108,10 @@
|
||||
|
||||
## 2. Enhanced toolsArg Functionality
|
||||
|
||||
### 2.1 Test Flow Control
|
||||
```typescript
|
||||
tap.test('conditional test', async (toolsArg) => {
|
||||
const result = await someOperation();
|
||||
|
||||
// Skip the rest of the test
|
||||
if (!result) {
|
||||
return toolsArg.skip('Precondition not met');
|
||||
}
|
||||
|
||||
// Conditional skipping
|
||||
await toolsArg.skipIf(condition, 'Reason for skipping');
|
||||
|
||||
// Mark test as todo
|
||||
await toolsArg.todo('Not implemented yet');
|
||||
});
|
||||
```
|
||||
|
||||
### 2.2 Test Metadata and Configuration
|
||||
```typescript
|
||||
tap.test('configurable test', async (toolsArg) => {
|
||||
// Set custom timeout
|
||||
toolsArg.timeout(5000);
|
||||
|
||||
// Retry on failure
|
||||
toolsArg.retry(3);
|
||||
|
||||
// Add tags for filtering
|
||||
toolsArg.tags(['slow', 'integration']);
|
||||
|
||||
// Set test priority
|
||||
toolsArg.priority('high');
|
||||
});
|
||||
```
|
||||
|
||||
### 2.3 Test Data and Context Sharing
|
||||
### 2.3 Test Data and Context Sharing (Partial)
|
||||
```typescript
|
||||
tap.test('data-driven test', async (toolsArg) => {
|
||||
// Access shared context
|
||||
const sharedData = toolsArg.context.get('sharedData');
|
||||
|
||||
// Set data for other tests
|
||||
toolsArg.context.set('resultData', computedValue);
|
||||
|
||||
// Parameterized test data
|
||||
// Parameterized test data (not yet implemented)
|
||||
const testData = toolsArg.data<TestInput>();
|
||||
expect(processData(testData)).toEqual(expected);
|
||||
});
|
||||
@ -70,32 +119,7 @@ tap.test('data-driven test', async (toolsArg) => {
|
||||
|
||||
## 3. Nested Tests and Test Suites
|
||||
|
||||
### 3.1 Test Grouping with describe()
|
||||
```typescript
|
||||
tap.describe('User Authentication', () => {
|
||||
tap.beforeEach(async (toolsArg) => {
|
||||
// Setup for each test in this suite
|
||||
await toolsArg.context.set('db', await createTestDatabase());
|
||||
});
|
||||
|
||||
tap.afterEach(async (toolsArg) => {
|
||||
// Cleanup after each test
|
||||
await toolsArg.context.get('db').cleanup();
|
||||
});
|
||||
|
||||
tap.test('should login with valid credentials', async (toolsArg) => {
|
||||
// Test implementation
|
||||
});
|
||||
|
||||
tap.describe('Password Reset', () => {
|
||||
tap.test('should send reset email', async (toolsArg) => {
|
||||
// Nested test
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 3.2 Hierarchical Test Organization
|
||||
### 3.2 Hierarchical Test Organization (Not yet implemented)
|
||||
- Support for multiple levels of nesting
|
||||
- Inherited context and configuration from parent suites
|
||||
- Aggregated reporting for test suites
|
||||
@ -103,15 +127,7 @@ tap.describe('User Authentication', () => {
|
||||
|
||||
## 4. Advanced Test Features
|
||||
|
||||
### 4.1 Snapshot Testing
|
||||
```typescript
|
||||
tap.test('component render', async (toolsArg) => {
|
||||
const output = renderComponent(props);
|
||||
|
||||
// Compare with stored snapshot
|
||||
await toolsArg.matchSnapshot(output, 'component-output');
|
||||
});
|
||||
```
|
||||
### 4.1 Snapshot Testing ✅ (Basic implementation complete)
|
||||
|
||||
### 4.2 Performance Benchmarking
|
||||
```typescript
|
||||
@ -129,25 +145,9 @@ tap.test('performance test', async (toolsArg) => {
|
||||
});
|
||||
```
|
||||
|
||||
### 4.3 Test Fixtures and Factories
|
||||
```typescript
|
||||
tap.test('with fixtures', async (toolsArg) => {
|
||||
// Create test fixtures
|
||||
const user = await toolsArg.fixture('user', { name: 'Test User' });
|
||||
const post = await toolsArg.fixture('post', { author: user });
|
||||
|
||||
// Use factory functions
|
||||
const users = await toolsArg.factory('user').createMany(5);
|
||||
});
|
||||
```
|
||||
|
||||
## 5. Test Execution Improvements
|
||||
|
||||
### 5.1 Parallel Test Execution
|
||||
- Run independent tests concurrently
|
||||
- Configurable concurrency limits
|
||||
- Resource pooling for shared resources
|
||||
- Proper isolation between parallel tests
|
||||
|
||||
### 5.2 Watch Mode
|
||||
- Automatically re-run tests on file changes
|
||||
@ -155,18 +155,15 @@ tap.test('with fixtures', async (toolsArg) => {
|
||||
- Fast feedback loop for development
|
||||
- Integration with IDE/editor plugins
|
||||
|
||||
### 5.3 Advanced Test Filtering
|
||||
### 5.3 Advanced Test Filtering (Partial)
|
||||
```typescript
|
||||
// Run tests by tags
|
||||
tstest --tags "unit,fast"
|
||||
|
||||
// Exclude tests by pattern
|
||||
// Exclude tests by pattern (not yet implemented)
|
||||
tstest --exclude "**/slow/**"
|
||||
|
||||
// Run only failed tests from last run
|
||||
// Run only failed tests from last run (not yet implemented)
|
||||
tstest --failed
|
||||
|
||||
// Run tests modified in git
|
||||
// Run tests modified in git (not yet implemented)
|
||||
tstest --changed
|
||||
```
|
||||
|
||||
@ -198,50 +195,52 @@ tstest --changed
|
||||
- Links to documentation
|
||||
- Code examples in error output
|
||||
|
||||
### 7.2 Interactive Mode
|
||||
- REPL for exploring test failures
|
||||
- Debugging integration
|
||||
- Step-through test execution
|
||||
- Interactive test data manipulation
|
||||
|
||||
### 7.3 VS Code Extension
|
||||
- Test explorer integration
|
||||
- Inline test results
|
||||
- CodeLens for running individual tests
|
||||
- Debugging support
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 1: Core Enhancements (Priority: High)
|
||||
1. Implement enhanced toolsArg methods (skip, skipIf, timeout, retry)
|
||||
2. Add basic test grouping with describe()
|
||||
3. Improve error reporting between tapbundle and tstest
|
||||
### Phase 1: Improved Internal Protocol (Priority: Critical) (NEW)
|
||||
1. Create ts_tapbundle_protocol directory with isomorphic protocol v2 implementation
|
||||
- Implement ProtocolEmitter class for message generation
|
||||
- Implement ProtocolParser class for message parsing
|
||||
- Define ProtocolMessage types and interfaces
|
||||
- Ensure all code is browser and Node.js compatible
|
||||
- Add tspublish.json to configure build order
|
||||
2. Update build configuration to compile ts_tapbundle_protocol first
|
||||
3. Replace TAP parser in tstest with Protocol V2 parser importing from dist_ts_tapbundle_protocol
|
||||
4. Replace TAP generation in tapbundle with Protocol V2 emitter importing from dist_ts_tapbundle_protocol
|
||||
5. Delete all v1 TAP parsing code from tstest
|
||||
6. Delete all v1 TAP generation code from tapbundle
|
||||
7. Test with real-world test suites containing special characters
|
||||
|
||||
### Phase 2: Advanced Features (Priority: Medium)
|
||||
1. Implement nested test suites
|
||||
2. Add snapshot testing
|
||||
3. Create test fixture system
|
||||
4. Implement parallel test execution
|
||||
### Phase 2: Test Configuration System (Priority: High)
|
||||
1. Implement tap.settings() API with TypeScript interfaces
|
||||
2. Add 00init.ts discovery and loading mechanism
|
||||
3. Implement settings inheritance and merge logic
|
||||
4. Apply settings to test execution (timeouts, retries, etc.)
|
||||
|
||||
### Phase 3: Developer Experience (Priority: Medium)
|
||||
### Phase 3: Enhanced Communication (Priority: High)
|
||||
1. Build on Protocol V2 for richer communication
|
||||
2. Implement real-time test progress API
|
||||
3. Add structured error reporting with diffs and traces
|
||||
|
||||
### Phase 4: Developer Experience (Priority: Medium)
|
||||
1. Add watch mode
|
||||
2. Implement custom reporters
|
||||
3. Create VS Code extension
|
||||
4. Add interactive debugging
|
||||
3. Complete advanced test filtering options
|
||||
4. Add performance benchmarking API
|
||||
|
||||
### Phase 4: Analytics and Performance (Priority: Low)
|
||||
### Phase 5: Analytics and Performance (Priority: Low)
|
||||
1. Build test analytics dashboard
|
||||
2. Add performance benchmarking
|
||||
3. Implement coverage integration
|
||||
4. Create trend analysis tools
|
||||
2. Implement coverage integration
|
||||
3. Create trend analysis tools
|
||||
4. Add test impact analysis
|
||||
|
||||
## Technical Considerations
|
||||
|
||||
### API Design Principles
|
||||
- Maintain backward compatibility
|
||||
- Clean, modern API design without legacy constraints
|
||||
- Progressive enhancement approach
|
||||
- Opt-in features to avoid breaking changes
|
||||
- Clear migration paths for new features
|
||||
- Well-documented features and APIs
|
||||
- Clear, simple interfaces
|
||||
|
||||
### Performance Goals
|
||||
- Minimal overhead for test execution
|
||||
|
287
readme.protocol.md
Normal file
287
readme.protocol.md
Normal file
@ -0,0 +1,287 @@
|
||||
# Improved Internal Protocol Design
|
||||
|
||||
## Current Issues with TAP Protocol
|
||||
|
||||
1. **Delimiter Conflict**: Using `#` for metadata conflicts with test descriptions containing `#`
|
||||
2. **Ambiguous Parsing**: No clear boundary between test name and metadata
|
||||
3. **Limited Extensibility**: Adding new metadata requires regex changes
|
||||
4. **Mixed Concerns**: Protocol data mixed with human-readable output
|
||||
|
||||
## Proposed Internal Protocol v2
|
||||
|
||||
### Design Principles
|
||||
|
||||
1. **Clear Separation**: Protocol data must be unambiguously separated from user content
|
||||
2. **Extensibility**: Easy to add new metadata without breaking parsers
|
||||
3. **Backwards Compatible**: Can coexist with standard TAP for gradual migration
|
||||
4. **Machine Readable**: Structured format for reliable parsing
|
||||
5. **Human Friendly**: Still readable in raw form
|
||||
|
||||
### Protocol Options
|
||||
|
||||
#### Option 1: Special Delimiters
|
||||
```
|
||||
ok 1 - test description ::TSTEST:: {"time":123,"retry":0}
|
||||
not ok 2 - another test ::TSTEST:: {"time":45,"error":"timeout"}
|
||||
ok 3 - skipped test ::TSTEST:: {"time":0,"skip":"not ready"}
|
||||
```
|
||||
|
||||
**Pros**:
|
||||
- Simple to implement
|
||||
- Backwards compatible with TAP parsers (they ignore the suffix)
|
||||
- Easy to parse with split()
|
||||
|
||||
**Cons**:
|
||||
- Still could conflict if test name contains `::TSTEST::`
|
||||
- Not standard TAP
|
||||
|
||||
#### Option 2: Separate Metadata Lines
|
||||
```
|
||||
ok 1 - test description
|
||||
::METADATA:: {"test":1,"time":123,"retry":0}
|
||||
not ok 2 - another test
|
||||
::METADATA:: {"test":2,"time":45,"error":"timeout"}
|
||||
```
|
||||
|
||||
**Pros**:
|
||||
- Complete separation of concerns
|
||||
- No chance of conflicts
|
||||
- Can include arbitrary metadata
|
||||
|
||||
**Cons**:
|
||||
- Requires correlation between lines
|
||||
- More complex parsing
|
||||
|
||||
#### Option 3: YAML Blocks (TAP 13 Compatible)
|
||||
```
|
||||
ok 1 - test description
|
||||
---
|
||||
time: 123
|
||||
retry: 0
|
||||
...
|
||||
not ok 2 - another test
|
||||
---
|
||||
time: 45
|
||||
error: timeout
|
||||
stack: |
|
||||
Error: timeout
|
||||
at Test.run (test.js:10:5)
|
||||
...
|
||||
```
|
||||
|
||||
**Pros**:
|
||||
- Standard TAP 13 feature
|
||||
- Structured data format
|
||||
- Human readable
|
||||
- Extensible
|
||||
|
||||
**Cons**:
|
||||
- More verbose
|
||||
- YAML parsing overhead
|
||||
|
||||
#### Option 4: Binary Protocol Markers (Recommended)
|
||||
```
|
||||
ok 1 - test description
|
||||
␛[TSTEST:eyJ0aW1lIjoxMjMsInJldHJ5IjowfQ==]␛
|
||||
not ok 2 - another test
|
||||
␛[TSTEST:eyJ0aW1lIjo0NSwiZXJyb3IiOiJ0aW1lb3V0In0=]␛
|
||||
```
|
||||
|
||||
Using ASCII escape character (␛ = \x1B) with base64 encoded JSON.
|
||||
|
||||
**Pros**:
|
||||
- Zero chance of accidental conflicts
|
||||
- Compact
|
||||
- Fast to parse
|
||||
- Invisible in most terminals
|
||||
|
||||
**Cons**:
|
||||
- Not human readable in raw form
|
||||
- Requires base64 encoding/decoding
|
||||
|
||||
### Recommended Implementation: Hybrid Approach
|
||||
|
||||
Use multiple strategies based on context:
|
||||
|
||||
1. **For timing and basic metadata**: Use structured delimiters
|
||||
```
|
||||
ok 1 - test name ⟦time:123,retry:0⟧
|
||||
```
|
||||
|
||||
2. **For complex data (errors, snapshots)**: Use separate protocol lines
|
||||
```
|
||||
ok 1 - test failed
|
||||
⟦TSTEST:ERROR⟧
|
||||
{"message":"Assertion failed","stack":"...","diff":"..."}
|
||||
⟦/TSTEST:ERROR⟧
|
||||
```
|
||||
|
||||
3. **For human-readable output**: Keep standard TAP comments
|
||||
```
|
||||
# Test suite: User Authentication
|
||||
ok 1 - should login
|
||||
```
|
||||
|
||||
### Implementation Plan
|
||||
|
||||
#### Phase 1: Parser Enhancement
|
||||
1. Add new protocol parser alongside existing TAP parser
|
||||
2. Support both old and new formats during transition
|
||||
3. Add protocol version negotiation
|
||||
|
||||
#### Phase 2: Metadata Structure
|
||||
```typescript
|
||||
interface TestMetadata {
|
||||
// Timing
|
||||
time: number; // milliseconds
|
||||
startTime?: number; // Unix timestamp
|
||||
endTime?: number; // Unix timestamp
|
||||
|
||||
// Status
|
||||
skip?: string; // skip reason
|
||||
todo?: string; // todo reason
|
||||
retry?: number; // retry attempt
|
||||
maxRetries?: number; // max retries allowed
|
||||
|
||||
// Error details
|
||||
error?: {
|
||||
message: string;
|
||||
stack?: string;
|
||||
diff?: string;
|
||||
actual?: any;
|
||||
expected?: any;
|
||||
};
|
||||
|
||||
// Test context
|
||||
file?: string; // source file
|
||||
line?: number; // line number
|
||||
column?: number; // column number
|
||||
|
||||
// Custom data
|
||||
tags?: string[]; // test tags
|
||||
custom?: Record<string, any>;
|
||||
}
|
||||
```
|
||||
|
||||
#### Phase 3: Protocol Messages
|
||||
|
||||
##### Success Message
|
||||
```
|
||||
ok 1 - user authentication works
|
||||
⟦TSTEST:META:{"time":123,"tags":["auth","unit"]}⟧
|
||||
```
|
||||
|
||||
##### Failure Message
|
||||
```
|
||||
not ok 2 - login fails with invalid password
|
||||
⟦TSTEST:META:{"time":45,"retry":1,"maxRetries":3}⟧
|
||||
⟦TSTEST:ERROR⟧
|
||||
{
|
||||
"message": "Expected 401 but got 500",
|
||||
"stack": "Error: Expected 401 but got 500\n at Test.run (auth.test.ts:25:10)",
|
||||
"actual": 500,
|
||||
"expected": 401
|
||||
}
|
||||
⟦/TSTEST:ERROR⟧
|
||||
```
|
||||
|
||||
##### Skip Message
|
||||
```
|
||||
ok 3 - database integration test ⟦TSTEST:SKIP:No database connection⟧
|
||||
```
|
||||
|
||||
##### Snapshot Communication
|
||||
```
|
||||
⟦TSTEST:SNAPSHOT:user-profile⟧
|
||||
{
|
||||
"name": "John Doe",
|
||||
"email": "john@example.com",
|
||||
"roles": ["user", "admin"]
|
||||
}
|
||||
⟦/TSTEST:SNAPSHOT⟧
|
||||
```
|
||||
|
||||
### Migration Strategy
|
||||
|
||||
1. **Version Detection**: First line indicates protocol version
|
||||
```
|
||||
⟦TSTEST:PROTOCOL:2.0⟧
|
||||
TAP version 13
|
||||
```
|
||||
|
||||
2. **Gradual Rollout**:
|
||||
- v1.10: Add protocol v2 parser, keep v1 generator
|
||||
- v1.11: Generate v2 by default, v1 with --legacy flag
|
||||
- v2.0: Remove v1 support
|
||||
|
||||
3. **Feature Flags**:
|
||||
```typescript
|
||||
tap.settings({
|
||||
protocol: 'v2', // or 'v1', 'auto'
|
||||
protocolFeatures: {
|
||||
structuredErrors: true,
|
||||
enhancedTiming: true,
|
||||
binaryMarkers: false
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Benefits of New Protocol
|
||||
|
||||
1. **Reliability**: No more regex fragility or description conflicts
|
||||
2. **Performance**: Faster parsing with clear boundaries
|
||||
3. **Extensibility**: Easy to add new metadata fields
|
||||
4. **Debugging**: Rich error information with stack traces and diffs
|
||||
5. **Integration**: Better IDE and CI/CD tool integration
|
||||
6. **Forward Compatible**: Room for future enhancements
|
||||
|
||||
### Example Parser Implementation
|
||||
|
||||
```typescript
|
||||
class ProtocolV2Parser {
|
||||
private readonly MARKER_START = '⟦TSTEST:';
|
||||
private readonly MARKER_END = '⟧';
|
||||
|
||||
parseMetadata(line: string): TestMetadata | null {
|
||||
const start = line.lastIndexOf(this.MARKER_START);
|
||||
if (start === -1) return null;
|
||||
|
||||
const end = line.indexOf(this.MARKER_END, start);
|
||||
if (end === -1) return null;
|
||||
|
||||
const content = line.substring(start + this.MARKER_START.length, end);
|
||||
const [type, data] = content.split(':', 2);
|
||||
|
||||
switch (type) {
|
||||
case 'META':
|
||||
return JSON.parse(data);
|
||||
case 'SKIP':
|
||||
return { skip: data };
|
||||
case 'TODO':
|
||||
return { todo: data };
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
parseTestLine(line: string): ParsedTest {
|
||||
// First extract any metadata
|
||||
const metadata = this.parseMetadata(line);
|
||||
|
||||
// Then parse the TAP part (without metadata)
|
||||
const cleanLine = this.removeMetadata(line);
|
||||
const tapResult = this.parseTAP(cleanLine);
|
||||
|
||||
return { ...tapResult, metadata };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Next Steps
|
||||
|
||||
1. Implement proof of concept with basic metadata support
|
||||
2. Test with real-world test suites for edge cases
|
||||
3. Benchmark parsing performance
|
||||
4. Get feedback from users
|
||||
5. Finalize protocol specification
|
||||
6. Implement in both tapbundle and tstest
|
3
test/debug.js
Normal file
3
test/debug.js
Normal file
@ -0,0 +1,3 @@
|
||||
// Direct run to see TAP output
|
||||
const { execSync } = require('child_process');
|
||||
console.log(execSync('tsx test/tapbundle/test.debug.ts', { cwd: '/mnt/data/lossless/git.zone/tstest' }).toString());
|
8
test/glob-test/another.spec.ts
Normal file
8
test/glob-test/another.spec.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { tap } from '../../ts_tapbundle/index.js';
|
||||
|
||||
tap.test('spec file test', async () => {
|
||||
console.log('This is a .spec.ts file that should be found by glob');
|
||||
return true;
|
||||
});
|
||||
|
||||
tap.start();
|
8
test/glob-test/nested/test.nested-glob.ts
Normal file
8
test/glob-test/nested/test.nested-glob.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { tap } from '../../../ts_tapbundle/index.js';
|
||||
|
||||
tap.test('nested glob pattern test', async () => {
|
||||
console.log('This test file is in a nested directory');
|
||||
return true;
|
||||
});
|
||||
|
||||
tap.start();
|
8
test/glob-test/test.glob-test.ts
Normal file
8
test/glob-test/test.glob-test.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { tap } from '../../ts_tapbundle/index.js';
|
||||
|
||||
tap.test('glob pattern test', async () => {
|
||||
console.log('This test file should be found by glob patterns');
|
||||
return true;
|
||||
});
|
||||
|
||||
tap.start();
|
@ -45,4 +45,11 @@ const test6 = tap.skip.test('my 6th test -> should fail after 1000ms', async (to
|
||||
await tools.delayFor(100);
|
||||
});
|
||||
|
||||
await tap.start();
|
||||
const testPromise = tap.start();
|
||||
|
||||
// Export promise for browser compatibility
|
||||
if (typeof globalThis !== 'undefined') {
|
||||
(globalThis as any).tapPromise = testPromise;
|
||||
}
|
||||
|
||||
export default testPromise;
|
19
test/tapbundle/test.debug.ts
Normal file
19
test/tapbundle/test.debug.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { tap, expect } from '../../ts_tapbundle/index.js';
|
||||
|
||||
// Simple test to debug TAP output
|
||||
tap.test('test 1', async () => {
|
||||
console.log('Test 1 running');
|
||||
expect(true).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('test 2 - skip', async (toolsArg) => {
|
||||
toolsArg.skip('Skipping test 2');
|
||||
expect(false).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('test 3', async () => {
|
||||
console.log('Test 3 running');
|
||||
expect(true).toBeTrue();
|
||||
});
|
||||
|
||||
tap.start();
|
101
test/tapbundle/test.describe.ts
Normal file
101
test/tapbundle/test.describe.ts
Normal file
@ -0,0 +1,101 @@
|
||||
import { tap, expect } from '../../ts_tapbundle/index.js';
|
||||
|
||||
// Global state for testing lifecycle hooks
|
||||
const lifecycleOrder: string[] = [];
|
||||
|
||||
tap.describe('Test Suite A', () => {
|
||||
tap.beforeEach(async (toolsArg) => {
|
||||
lifecycleOrder.push('Suite A - beforeEach');
|
||||
});
|
||||
|
||||
tap.afterEach(async (toolsArg) => {
|
||||
lifecycleOrder.push('Suite A - afterEach');
|
||||
});
|
||||
|
||||
tap.test('test 1 in suite A', async (toolsArg) => {
|
||||
lifecycleOrder.push('Test 1');
|
||||
expect(true).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('test 2 in suite A', async (toolsArg) => {
|
||||
lifecycleOrder.push('Test 2');
|
||||
expect(true).toBeTrue();
|
||||
});
|
||||
|
||||
tap.describe('Nested Suite B', () => {
|
||||
tap.beforeEach(async (toolsArg) => {
|
||||
lifecycleOrder.push('Suite B - beforeEach');
|
||||
});
|
||||
|
||||
tap.afterEach(async (toolsArg) => {
|
||||
lifecycleOrder.push('Suite B - afterEach');
|
||||
});
|
||||
|
||||
tap.test('test 1 in nested suite B', async (toolsArg) => {
|
||||
lifecycleOrder.push('Nested Test 1');
|
||||
expect(true).toBeTrue();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Test outside any suite
|
||||
tap.test('test outside suites', async (toolsArg) => {
|
||||
lifecycleOrder.push('Outside Test');
|
||||
expect(true).toBeTrue();
|
||||
});
|
||||
|
||||
tap.describe('Test Suite with errors', () => {
|
||||
tap.beforeEach(async (toolsArg) => {
|
||||
// Setup that might fail
|
||||
const data = await Promise.resolve({ value: 42 });
|
||||
toolsArg.testData = data;
|
||||
});
|
||||
|
||||
tap.test('test with error', async (toolsArg) => {
|
||||
// Verify that data from beforeEach is available
|
||||
expect(toolsArg.testData).toBeDefined();
|
||||
expect(toolsArg.testData.value).toEqual(42);
|
||||
|
||||
// Test that error handling works by catching an error
|
||||
try {
|
||||
throw new Error('Intentional error');
|
||||
} catch (error) {
|
||||
expect(error.message).toEqual('Intentional error');
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('test with skip in suite', async (toolsArg) => {
|
||||
toolsArg.skip('Skipping this test in a suite');
|
||||
expect(false).toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
// Verify lifecycle order - this test runs last to check if all hooks were called properly
|
||||
tap.test('verify lifecycle hook order', async (toolsArg) => {
|
||||
// Wait a bit to ensure all tests have completed
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
console.log('Lifecycle order:', lifecycleOrder);
|
||||
|
||||
// Check that the tests we expect to have run actually did
|
||||
expect(lifecycleOrder).toContain('Test 1');
|
||||
expect(lifecycleOrder).toContain('Test 2');
|
||||
expect(lifecycleOrder).toContain('Nested Test 1');
|
||||
|
||||
// Check that beforeEach was called before each test in Suite A
|
||||
const test1Index = lifecycleOrder.indexOf('Test 1');
|
||||
expect(test1Index).toBeGreaterThan(-1);
|
||||
const beforeTest1 = lifecycleOrder.slice(0, test1Index);
|
||||
expect(beforeTest1).toContain('Suite A - beforeEach');
|
||||
|
||||
// Check that afterEach was called after test 1
|
||||
const afterTest1 = lifecycleOrder.slice(test1Index + 1);
|
||||
expect(afterTest1).toContain('Suite A - afterEach');
|
||||
|
||||
// Check nested suite lifecycle
|
||||
const nestedTest1Index = lifecycleOrder.indexOf('Nested Test 1');
|
||||
expect(nestedTest1Index).toBeGreaterThan(-1);
|
||||
const beforeNestedTest1 = lifecycleOrder.slice(0, nestedTest1Index);
|
||||
expect(beforeNestedTest1).toContain('Suite B - beforeEach');
|
||||
});
|
||||
|
||||
tap.start();
|
120
test/tapbundle/test.fixtures.ts
Normal file
120
test/tapbundle/test.fixtures.ts
Normal file
@ -0,0 +1,120 @@
|
||||
import { tap, TapTools } from '../../ts_tapbundle/index.js';
|
||||
import { expect } from '@push.rocks/smartexpect';
|
||||
|
||||
// Define fixture factories
|
||||
interface User {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
role: string;
|
||||
}
|
||||
|
||||
interface Post {
|
||||
id: number;
|
||||
title: string;
|
||||
content: string;
|
||||
authorId: number;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
// Define user fixture factory
|
||||
TapTools.defineFixture<User>('user', (data) => {
|
||||
const id = data?.id || Math.floor(Math.random() * 10000);
|
||||
return {
|
||||
id,
|
||||
name: data?.name || `Test User ${id}`,
|
||||
email: data?.email || `user${id}@test.com`,
|
||||
role: data?.role || 'user'
|
||||
};
|
||||
});
|
||||
|
||||
// Define post fixture factory
|
||||
TapTools.defineFixture<Post>('post', async (data) => {
|
||||
const id = data?.id || Math.floor(Math.random() * 10000);
|
||||
return {
|
||||
id,
|
||||
title: data?.title || `Post ${id}`,
|
||||
content: data?.content || `Content for post ${id}`,
|
||||
authorId: data?.authorId || 1,
|
||||
tags: data?.tags || ['test', 'sample']
|
||||
};
|
||||
});
|
||||
|
||||
tap.describe('Fixture System', () => {
|
||||
tap.afterEach(async () => {
|
||||
// Clean up fixtures after each test
|
||||
await TapTools.cleanupFixtures();
|
||||
});
|
||||
|
||||
tap.tags('unit', 'fixtures')
|
||||
.test('should create a simple fixture', async (toolsArg) => {
|
||||
const user = await toolsArg.fixture<User>('user');
|
||||
|
||||
expect(user).toHaveProperty('id');
|
||||
expect(user).toHaveProperty('name');
|
||||
expect(user).toHaveProperty('email');
|
||||
expect(user.role).toEqual('user');
|
||||
});
|
||||
|
||||
tap.tags('unit', 'fixtures')
|
||||
.test('should create fixture with custom data', async (toolsArg) => {
|
||||
const admin = await toolsArg.fixture<User>('user', {
|
||||
name: 'Admin User',
|
||||
role: 'admin'
|
||||
});
|
||||
|
||||
expect(admin.name).toEqual('Admin User');
|
||||
expect(admin.role).toEqual('admin');
|
||||
expect(admin.email).toContain('@test.com');
|
||||
});
|
||||
|
||||
tap.tags('unit', 'fixtures')
|
||||
.test('should create multiple fixtures with factory', async (toolsArg) => {
|
||||
const userFactory = toolsArg.factory<User>('user');
|
||||
const users = await userFactory.createMany(3);
|
||||
|
||||
// Try different approach
|
||||
expect(users.length).toEqual(3);
|
||||
expect(users[0].id).not.toEqual(users[1].id);
|
||||
expect(users[0].email).not.toEqual(users[1].email);
|
||||
});
|
||||
|
||||
tap.tags('unit', 'fixtures')
|
||||
.test('should create fixtures with custom data per instance', async (toolsArg) => {
|
||||
const postFactory = toolsArg.factory<Post>('post');
|
||||
const posts = await postFactory.createMany(3, (index) => ({
|
||||
title: `Post ${index + 1}`,
|
||||
tags: [`tag${index + 1}`]
|
||||
}));
|
||||
|
||||
expect(posts[0].title).toEqual('Post 1');
|
||||
expect(posts[1].title).toEqual('Post 2');
|
||||
expect(posts[2].title).toEqual('Post 3');
|
||||
|
||||
expect(posts[0].tags).toContain('tag1');
|
||||
expect(posts[1].tags).toContain('tag2');
|
||||
});
|
||||
|
||||
tap.tags('unit', 'fixtures')
|
||||
.test('should handle related fixtures', async (toolsArg) => {
|
||||
const user = await toolsArg.fixture<User>('user', { name: 'Author' });
|
||||
const post = await toolsArg.fixture<Post>('post', {
|
||||
title: 'My Article',
|
||||
authorId: user.id
|
||||
});
|
||||
|
||||
expect(post.authorId).toEqual(user.id);
|
||||
});
|
||||
|
||||
tap.tags('unit', 'fixtures', 'error')
|
||||
.test('should throw error for undefined fixture', async (toolsArg) => {
|
||||
try {
|
||||
await toolsArg.fixture('nonexistent');
|
||||
expect(true).toBeFalse(); // Should not reach here
|
||||
} catch (error: any) {
|
||||
expect(error.message).toContain('Fixture \'nonexistent\' not found');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
tap.start();
|
32
test/tapbundle/test.fluent-syntax.ts
Normal file
32
test/tapbundle/test.fluent-syntax.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { tap, expect } from '../../ts_tapbundle/index.js';
|
||||
|
||||
// Test with fluent syntax
|
||||
tap.tags('unit', 'fluent')
|
||||
.priority('high')
|
||||
.test('test with fluent syntax', async (toolsArg) => {
|
||||
expect(true).toBeTrue();
|
||||
toolsArg.context.set('fluentTest', 'works');
|
||||
});
|
||||
|
||||
// Chain multiple settings
|
||||
tap.tags('integration')
|
||||
.priority('low')
|
||||
.retry(3)
|
||||
.timeout(5000)
|
||||
.test('test with multiple settings', async (toolsArg) => {
|
||||
expect(true).toBeTrue();
|
||||
});
|
||||
|
||||
// Test context access from fluent test
|
||||
tap.tags('unit')
|
||||
.test('verify fluent context', async (toolsArg) => {
|
||||
const fluentValue = toolsArg.context.get('fluentTest');
|
||||
expect(fluentValue).toEqual('works');
|
||||
});
|
||||
|
||||
// Test without tags - should show all tests run without filtering
|
||||
tap.test('regular test without tags', async (toolsArg) => {
|
||||
expect(true).toBeTrue();
|
||||
});
|
||||
|
||||
tap.start();
|
52
test/tapbundle/test.snapshot.ts
Normal file
52
test/tapbundle/test.snapshot.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { tap, expect } from '../../ts_tapbundle/index.js';
|
||||
|
||||
// Test basic snapshot functionality
|
||||
tap.tags('unit', 'snapshot')
|
||||
.test('should match string snapshot', async (toolsArg) => {
|
||||
const testString = 'Hello, World!';
|
||||
await toolsArg.matchSnapshot(testString);
|
||||
});
|
||||
|
||||
// Test object snapshot
|
||||
tap.tags('unit', 'snapshot')
|
||||
.test('should match object snapshot', async (toolsArg) => {
|
||||
const testObject = {
|
||||
name: 'Test User',
|
||||
age: 30,
|
||||
hobbies: ['reading', 'coding', 'gaming'],
|
||||
metadata: {
|
||||
created: '2024-01-01',
|
||||
updated: '2024-01-15'
|
||||
}
|
||||
};
|
||||
await toolsArg.matchSnapshot(testObject);
|
||||
});
|
||||
|
||||
// Test named snapshots
|
||||
tap.tags('unit', 'snapshot')
|
||||
.test('should handle multiple named snapshots', async (toolsArg) => {
|
||||
const config1 = { version: '1.0.0', features: ['a', 'b'] };
|
||||
const config2 = { version: '2.0.0', features: ['a', 'b', 'c'] };
|
||||
|
||||
await toolsArg.matchSnapshot(config1, 'config_v1');
|
||||
await toolsArg.matchSnapshot(config2, 'config_v2');
|
||||
});
|
||||
|
||||
// Test dynamic content with snapshot
|
||||
tap.tags('unit', 'snapshot')
|
||||
.test('should handle template snapshot', async (toolsArg) => {
|
||||
const template = `
|
||||
<div class="container">
|
||||
<h1>Welcome</h1>
|
||||
<p>This is a test template</p>
|
||||
<ul>
|
||||
<li>Item 1</li>
|
||||
<li>Item 2</li>
|
||||
</ul>
|
||||
</div>
|
||||
`.trim();
|
||||
|
||||
await toolsArg.matchSnapshot(template, 'html_template');
|
||||
});
|
||||
|
||||
tap.start();
|
49
test/tapbundle/test.tags-context.ts
Normal file
49
test/tapbundle/test.tags-context.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { tap, expect } from '../../ts_tapbundle/index.js';
|
||||
|
||||
// First test sets some data and has tags
|
||||
tap.tags('unit', 'context')
|
||||
.priority('high')
|
||||
.test('test with tags and context setting', async (toolsArg) => {
|
||||
// Set some data in context
|
||||
toolsArg.context.set('testData', { value: 42 });
|
||||
toolsArg.context.set('users', ['alice', 'bob']);
|
||||
|
||||
expect(true).toBeTrue();
|
||||
});
|
||||
|
||||
// Second test reads the context data
|
||||
tap.tags('unit', 'context')
|
||||
.test('test reading context', async (toolsArg) => {
|
||||
// Read data from context
|
||||
const testData = toolsArg.context.get('testData');
|
||||
const users = toolsArg.context.get('users');
|
||||
|
||||
expect(testData).toEqual({ value: 42 });
|
||||
expect(users).toContain('alice');
|
||||
expect(users).toContain('bob');
|
||||
});
|
||||
|
||||
// Test without tags - should be skipped when filtering by tags
|
||||
tap.test('test without tags', async (toolsArg) => {
|
||||
expect(true).toBeTrue();
|
||||
});
|
||||
|
||||
// Test with different tags
|
||||
tap.tags('integration')
|
||||
.priority('low')
|
||||
.test('integration test', async (toolsArg) => {
|
||||
expect(true).toBeTrue();
|
||||
});
|
||||
|
||||
// Test context cleanup
|
||||
tap.tags('unit')
|
||||
.test('test context operations', async (toolsArg) => {
|
||||
// Set and delete
|
||||
toolsArg.context.set('temp', 'value');
|
||||
expect(toolsArg.context.get('temp')).toEqual('value');
|
||||
|
||||
toolsArg.context.delete('temp');
|
||||
expect(toolsArg.context.get('temp')).toBeUndefined();
|
||||
});
|
||||
|
||||
tap.start();
|
85
test/tapbundle/test.toolsarg.ts
Normal file
85
test/tapbundle/test.toolsarg.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import { tap, expect } from '../../ts_tapbundle/index.js';
|
||||
|
||||
// Test skip functionality
|
||||
tap.test('should skip a test with skip()', async (toolsArg) => {
|
||||
toolsArg.skip('This test is skipped');
|
||||
// This code should not run
|
||||
expect(false).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('should conditionally skip with skipIf()', async (toolsArg) => {
|
||||
const shouldSkip = true;
|
||||
toolsArg.skipIf(shouldSkip, 'Condition met, skipping');
|
||||
// This code should not run
|
||||
expect(false).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('should not skip when skipIf condition is false', async (toolsArg) => {
|
||||
const shouldSkip = false;
|
||||
toolsArg.skipIf(shouldSkip, 'Should not skip');
|
||||
// This code should run
|
||||
expect(true).toBeTrue();
|
||||
});
|
||||
|
||||
// Test todo functionality
|
||||
tap.test('should mark test as todo', async (toolsArg) => {
|
||||
toolsArg.todo('Not implemented yet');
|
||||
// Test code that would be implemented later
|
||||
expect(true).toBeTrue();
|
||||
});
|
||||
|
||||
// Test timeout functionality
|
||||
tap.test('should set custom timeout', async (toolsArg) => {
|
||||
toolsArg.timeout(5000);
|
||||
// Simulate a task that takes 100ms
|
||||
await toolsArg.delayFor(100);
|
||||
expect(true).toBeTrue();
|
||||
});
|
||||
|
||||
// This test is expected to fail due to timeout
|
||||
tap.test('should timeout when exceeding limit', async (toolsArg) => {
|
||||
toolsArg.timeout(100);
|
||||
// This test will timeout and be marked as failed by the test runner
|
||||
await toolsArg.delayFor(2000);
|
||||
// This line should not be reached due to timeout
|
||||
});
|
||||
|
||||
tap.test('timeout should work properly', async (toolsArg) => {
|
||||
toolsArg.timeout(200);
|
||||
// This test should complete successfully within the timeout
|
||||
await toolsArg.delayFor(50);
|
||||
expect(true).toBeTrue();
|
||||
});
|
||||
|
||||
// Test retry functionality
|
||||
tap.retry(3)
|
||||
.test('should retry on failure', async (toolsArg) => {
|
||||
// Use retry count to determine success
|
||||
const currentRetry = toolsArg.retryCount;
|
||||
|
||||
// Fail on first two attempts (0 and 1), succeed on third (2)
|
||||
if (currentRetry < 2) {
|
||||
throw new Error(`Attempt ${currentRetry + 1} failed`);
|
||||
}
|
||||
|
||||
expect(currentRetry).toEqual(2);
|
||||
});
|
||||
|
||||
tap.test('should expose retry count', async (toolsArg) => {
|
||||
toolsArg.retry(2);
|
||||
|
||||
// The retry count should be available
|
||||
expect(toolsArg.retryCount).toBeLessThanOrEqual(2);
|
||||
expect(true).toBeTrue();
|
||||
});
|
||||
|
||||
// Test allowFailure
|
||||
tap.test('should allow failure', async (toolsArg) => {
|
||||
// Just verify that allowFailure() can be called without throwing
|
||||
toolsArg.allowFailure();
|
||||
expect(true).toBeTrue();
|
||||
// Note: In a real implementation, we would see "please note: failure allowed!"
|
||||
// in the output when this test fails, but the test itself will still be marked as failed
|
||||
});
|
||||
|
||||
tap.start();
|
@ -17,9 +17,9 @@ const test3 = tap.test(
|
||||
async () => {
|
||||
expect(
|
||||
(await test1.testPromise).hrtMeasurement.milliSeconds <
|
||||
(await test2).hrtMeasurement.milliSeconds,
|
||||
(await test2.testPromise).hrtMeasurement.milliSeconds,
|
||||
).toBeTrue();
|
||||
expect((await test2.testPromise).hrtMeasurement.milliSeconds > 1000).toBeTrue();
|
||||
expect((await test2.testPromise).hrtMeasurement.milliSeconds >= 1000).toBeTrue();
|
||||
},
|
||||
);
|
||||
|
||||
@ -46,4 +46,4 @@ const test7 = tap.test('my 7th test -> should print a colored string', async (to
|
||||
console.log(cs);
|
||||
});
|
||||
|
||||
tap.start();
|
||||
tap.start();
|
16
test/tstest/test-parallel-demo.ts
Normal file
16
test/tstest/test-parallel-demo.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { tap, expect } from '../../ts_tapbundle/index.js';
|
||||
import * as fs from 'fs';
|
||||
|
||||
// Test to demonstrate parallel execution timing - run with glob pattern
|
||||
// This will give us a clear view of execution order with timestamps
|
||||
|
||||
const timestamp = () => new Date().toISOString().substr(11, 12);
|
||||
|
||||
tap.test('demo test in main file', async (toolsArg) => {
|
||||
console.log(`[${timestamp()}] Test parallel demo started`);
|
||||
await toolsArg.delayFor(1000);
|
||||
console.log(`[${timestamp()}] Test parallel demo completed`);
|
||||
expect(true).toBeTrue();
|
||||
});
|
||||
|
||||
tap.start();
|
11
test/tstest/test.api.para__2.ts
Normal file
11
test/tstest/test.api.para__2.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { tap, expect } from '../../ts_tapbundle/index.js';
|
||||
|
||||
// This test runs in parallel group 2
|
||||
tap.test('api test in parallel group 2', async (toolsArg) => {
|
||||
console.log('API test started');
|
||||
await toolsArg.delayFor(800);
|
||||
console.log('API test completed');
|
||||
expect(true).toBeTrue();
|
||||
});
|
||||
|
||||
tap.start();
|
13
test/tstest/test.auth.para__1.ts
Normal file
13
test/tstest/test.auth.para__1.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { tap, expect } from '../../ts_tapbundle/index.js';
|
||||
|
||||
// This test runs in parallel group 1
|
||||
const timestamp = () => new Date().toISOString().substr(11, 12);
|
||||
|
||||
tap.test('auth test in parallel group 1', async (toolsArg) => {
|
||||
console.log(`[${timestamp()}] Auth test started`);
|
||||
await toolsArg.delayFor(1000);
|
||||
console.log(`[${timestamp()}] Auth test completed`);
|
||||
expect(true).toBeTrue();
|
||||
});
|
||||
|
||||
tap.start();
|
11
test/tstest/test.db.para__2.ts
Normal file
11
test/tstest/test.db.para__2.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { tap, expect } from '../../ts_tapbundle/index.js';
|
||||
|
||||
// This test runs in parallel group 2
|
||||
tap.test('db test in parallel group 2', async (toolsArg) => {
|
||||
console.log('DB test started');
|
||||
await toolsArg.delayFor(800);
|
||||
console.log('DB test completed');
|
||||
expect(true).toBeTrue();
|
||||
});
|
||||
|
||||
tap.start();
|
10
test/tstest/test.serial1.ts
Normal file
10
test/tstest/test.serial1.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { tap, expect } from '../../ts_tapbundle/index.js';
|
||||
|
||||
// This test runs serially (no para__ in filename)
|
||||
tap.test('serial test 1', async (toolsArg) => {
|
||||
await toolsArg.delayFor(500);
|
||||
console.log('Serial test 1 completed');
|
||||
expect(true).toBeTrue();
|
||||
});
|
||||
|
||||
tap.start();
|
10
test/tstest/test.serial2.ts
Normal file
10
test/tstest/test.serial2.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { tap, expect } from '../../ts_tapbundle/index.js';
|
||||
|
||||
// This test runs serially (no para__ in filename)
|
||||
tap.test('serial test 2', async (toolsArg) => {
|
||||
await toolsArg.delayFor(500);
|
||||
console.log('Serial test 2 completed');
|
||||
expect(true).toBeTrue();
|
||||
});
|
||||
|
||||
tap.start();
|
13
test/tstest/test.user.para__1.ts
Normal file
13
test/tstest/test.user.para__1.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { tap, expect } from '../../ts_tapbundle/index.js';
|
||||
|
||||
// This test runs in parallel group 1
|
||||
const timestamp = () => new Date().toISOString().substr(11, 12);
|
||||
|
||||
tap.test('user test in parallel group 1', async (toolsArg) => {
|
||||
console.log(`[${timestamp()}] User test started`);
|
||||
await toolsArg.delayFor(1000);
|
||||
console.log(`[${timestamp()}] User test completed`);
|
||||
expect(true).toBeTrue();
|
||||
});
|
||||
|
||||
tap.start();
|
@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@git.zone/tstest',
|
||||
version: '1.6.0',
|
||||
version: '2.0.0',
|
||||
description: 'a test utility to run tests that match test/**/*.ts'
|
||||
}
|
||||
|
75
ts/index.ts
75
ts/index.ts
@ -12,6 +12,10 @@ export const runCli = async () => {
|
||||
const args = process.argv.slice(2);
|
||||
const logOptions: LogOptions = {};
|
||||
let testPath: string | null = null;
|
||||
let tags: string[] = [];
|
||||
let startFromFile: number | null = null;
|
||||
let stopAtFile: number | null = null;
|
||||
let timeoutSeconds: number | null = null;
|
||||
|
||||
// Parse options
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
@ -36,6 +40,50 @@ export const runCli = async () => {
|
||||
case '--logfile':
|
||||
logOptions.logFile = true; // Set this as a flag, not a value
|
||||
break;
|
||||
case '--tags':
|
||||
if (i + 1 < args.length) {
|
||||
tags = args[++i].split(',');
|
||||
}
|
||||
break;
|
||||
case '--startFrom':
|
||||
if (i + 1 < args.length) {
|
||||
const value = parseInt(args[++i], 10);
|
||||
if (isNaN(value) || value < 1) {
|
||||
console.error('Error: --startFrom must be a positive integer');
|
||||
process.exit(1);
|
||||
}
|
||||
startFromFile = value;
|
||||
} else {
|
||||
console.error('Error: --startFrom requires a number argument');
|
||||
process.exit(1);
|
||||
}
|
||||
break;
|
||||
case '--stopAt':
|
||||
if (i + 1 < args.length) {
|
||||
const value = parseInt(args[++i], 10);
|
||||
if (isNaN(value) || value < 1) {
|
||||
console.error('Error: --stopAt must be a positive integer');
|
||||
process.exit(1);
|
||||
}
|
||||
stopAtFile = value;
|
||||
} else {
|
||||
console.error('Error: --stopAt requires a number argument');
|
||||
process.exit(1);
|
||||
}
|
||||
break;
|
||||
case '--timeout':
|
||||
if (i + 1 < args.length) {
|
||||
const value = parseInt(args[++i], 10);
|
||||
if (isNaN(value) || value < 1) {
|
||||
console.error('Error: --timeout must be a positive integer (seconds)');
|
||||
process.exit(1);
|
||||
}
|
||||
timeoutSeconds = value;
|
||||
} else {
|
||||
console.error('Error: --timeout requires a number argument (seconds)');
|
||||
process.exit(1);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (!arg.startsWith('-')) {
|
||||
testPath = arg;
|
||||
@ -43,15 +91,25 @@ export const runCli = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Validate test file range options
|
||||
if (startFromFile !== null && stopAtFile !== null && startFromFile > stopAtFile) {
|
||||
console.error('Error: --startFrom cannot be greater than --stopAt');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
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');
|
||||
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');
|
||||
console.error(' --tags <tags> Run only tests with specified tags (comma-separated)');
|
||||
console.error(' --startFrom <n> Start running from test file number n');
|
||||
console.error(' --stopAt <n> Stop running at test file number n');
|
||||
console.error(' --timeout <s> Timeout test files after s seconds');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@ -66,6 +124,11 @@ export const runCli = async () => {
|
||||
executionMode = TestExecutionMode.DIRECTORY;
|
||||
}
|
||||
|
||||
const tsTestInstance = new TsTest(process.cwd(), testPath, executionMode, logOptions);
|
||||
const tsTestInstance = new TsTest(process.cwd(), testPath, executionMode, logOptions, tags, startFromFile, stopAtFile, timeoutSeconds);
|
||||
await tsTestInstance.run();
|
||||
};
|
||||
|
||||
// Execute CLI when this file is run directly
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
runCli();
|
||||
}
|
||||
|
@ -1,3 +1,3 @@
|
||||
{
|
||||
"order": 2
|
||||
"order": 4
|
||||
}
|
@ -10,6 +10,7 @@ import { TsTestLogger } from './tstest.logging.js';
|
||||
|
||||
export class TapCombinator {
|
||||
tapParserStore: TapParser[] = [];
|
||||
skippedFiles: string[] = [];
|
||||
private logger: TsTestLogger;
|
||||
|
||||
constructor(logger: TsTestLogger) {
|
||||
@ -19,10 +20,14 @@ export class TapCombinator {
|
||||
addTapParser(tapParserArg: TapParser) {
|
||||
this.tapParserStore.push(tapParserArg);
|
||||
}
|
||||
|
||||
addSkippedFile(filename: string) {
|
||||
this.skippedFiles.push(filename);
|
||||
}
|
||||
|
||||
evaluate() {
|
||||
// Call the logger's summary method
|
||||
this.logger.summary();
|
||||
// Call the logger's summary method with skipped files
|
||||
this.logger.summary(this.skippedFiles);
|
||||
|
||||
// Check for failures
|
||||
let failGlobal = false;
|
||||
|
@ -16,7 +16,7 @@ export class TapParser {
|
||||
expectedTests: number;
|
||||
receivedTests: number;
|
||||
|
||||
testStatusRegex = /(ok|not\sok)\s([0-9]+)\s-\s(.*)\s#\stime=(.*)ms$/;
|
||||
testStatusRegex = /(ok|not\sok)\s([0-9]+)\s-\s(.*?)(\s#\s(.*))?$/;
|
||||
activeTapTestResult: TapTestResult;
|
||||
collectingErrorDetails: boolean = false;
|
||||
currentTestError: string[] = [];
|
||||
@ -31,6 +31,36 @@ export class TapParser {
|
||||
constructor(public fileName: string, logger?: TsTestLogger) {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle test file timeout
|
||||
*/
|
||||
public handleTimeout(timeoutSeconds: number) {
|
||||
// If no tests have been defined yet, set expected to 1
|
||||
if (this.expectedTests === 0) {
|
||||
this.expectedTests = 1;
|
||||
}
|
||||
|
||||
// Create a fake failing test result for timeout
|
||||
this._getNewTapTestResult();
|
||||
this.activeTapTestResult.testOk = false;
|
||||
this.activeTapTestResult.testSettled = true;
|
||||
this.testStore.push(this.activeTapTestResult);
|
||||
|
||||
// Log the timeout error
|
||||
if (this.logger) {
|
||||
// First log the test result
|
||||
this.logger.testResult(
|
||||
`Test file timeout`,
|
||||
false,
|
||||
timeoutSeconds * 1000,
|
||||
`Error: Test file exceeded timeout of ${timeoutSeconds} seconds`
|
||||
);
|
||||
this.logger.testErrorDetails(`Test execution was terminated after ${timeoutSeconds} seconds`);
|
||||
}
|
||||
|
||||
// Don't call evaluateFinalResult here, let the caller handle it
|
||||
}
|
||||
|
||||
private _getNewTapTestResult() {
|
||||
this.activeTapTestResult = new TapTestResult(this.testStore.length + 1);
|
||||
@ -69,7 +99,7 @@ export class TapParser {
|
||||
} else if (this.testStatusRegex.test(logLine)) {
|
||||
logLineIsTapProtocol = true;
|
||||
const regexResult = this.testStatusRegex.exec(logLine);
|
||||
const testId = parseInt(regexResult[2]);
|
||||
// const testId = parseInt(regexResult[2]); // Currently unused
|
||||
const testOk = (() => {
|
||||
if (regexResult[1] === 'ok') {
|
||||
return true;
|
||||
@ -77,15 +107,29 @@ export class TapParser {
|
||||
return false;
|
||||
})();
|
||||
|
||||
const testSubject = regexResult[3];
|
||||
const testDuration = parseInt(regexResult[4]);
|
||||
|
||||
// test for protocol error
|
||||
if (testId !== this.activeTapTestResult.id) {
|
||||
if (this.logger) {
|
||||
this.logger.error('Something is strange! Test Ids are not equal!');
|
||||
const testSubject = regexResult[3].trim();
|
||||
const testMetadata = regexResult[5]; // This will be either "time=XXXms" or "SKIP reason" or "TODO reason"
|
||||
|
||||
let testDuration = 0;
|
||||
|
||||
if (testMetadata) {
|
||||
const timeMatch = testMetadata.match(/time=(\d+)ms/);
|
||||
// const skipMatch = testMetadata.match(/SKIP\s*(.*)/); // Currently unused
|
||||
// const todoMatch = testMetadata.match(/TODO\s*(.*)/); // Currently unused
|
||||
|
||||
if (timeMatch) {
|
||||
testDuration = parseInt(timeMatch[1]);
|
||||
}
|
||||
// Skip/todo handling could be added here in the future
|
||||
}
|
||||
|
||||
// test for protocol error - disabled as it's not critical
|
||||
// The test ID mismatch can occur when tests are filtered, skipped, or use todo
|
||||
// if (testId !== this.activeTapTestResult.id) {
|
||||
// if (this.logger) {
|
||||
// this.logger.error('Something is strange! Test Ids are not equal!');
|
||||
// }
|
||||
// }
|
||||
this.activeTapTestResult.setTestResult(testOk);
|
||||
|
||||
if (testOk) {
|
||||
@ -107,27 +151,41 @@ export class TapParser {
|
||||
this.activeTapTestResult.addLogLine(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');
|
||||
// Check for snapshot communication
|
||||
const snapshotMatch = logLine.match(/###SNAPSHOT###(.+)###SNAPSHOT###/);
|
||||
if (snapshotMatch) {
|
||||
const base64Data = snapshotMatch[1];
|
||||
try {
|
||||
const snapshotData = JSON.parse(Buffer.from(base64Data, 'base64').toString());
|
||||
this.handleSnapshot(snapshotData);
|
||||
} catch (error) {
|
||||
if (this.logger) {
|
||||
this.logger.testErrorDetails(errorMessage);
|
||||
this.logger.testConsoleOutput(`Error parsing snapshot data: ${error.message}`);
|
||||
}
|
||||
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);
|
||||
} else {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -205,6 +263,59 @@ export class TapParser {
|
||||
public async handleTapLog(tapLog: string) {
|
||||
this._processLog(tapLog);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle snapshot data from the test
|
||||
*/
|
||||
private async handleSnapshot(snapshotData: { path: string; content: string; action: string }) {
|
||||
try {
|
||||
const smartfile = await import('@push.rocks/smartfile');
|
||||
|
||||
if (snapshotData.action === 'compare') {
|
||||
// Try to read existing snapshot
|
||||
try {
|
||||
const existingSnapshot = await smartfile.fs.toStringSync(snapshotData.path);
|
||||
if (existingSnapshot !== snapshotData.content) {
|
||||
// Snapshot mismatch
|
||||
if (this.logger) {
|
||||
this.logger.testConsoleOutput(`Snapshot mismatch: ${snapshotData.path}`);
|
||||
this.logger.testConsoleOutput(`Expected:\n${existingSnapshot}`);
|
||||
this.logger.testConsoleOutput(`Received:\n${snapshotData.content}`);
|
||||
}
|
||||
// TODO: Communicate failure back to the test
|
||||
} else {
|
||||
if (this.logger) {
|
||||
this.logger.testConsoleOutput(`Snapshot matched: ${snapshotData.path}`);
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.code === 'ENOENT') {
|
||||
// Snapshot doesn't exist, create it
|
||||
const dirPath = snapshotData.path.substring(0, snapshotData.path.lastIndexOf('/'));
|
||||
await smartfile.fs.ensureDir(dirPath);
|
||||
await smartfile.memory.toFs(snapshotData.content, snapshotData.path);
|
||||
if (this.logger) {
|
||||
this.logger.testConsoleOutput(`Snapshot created: ${snapshotData.path}`);
|
||||
}
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
} else if (snapshotData.action === 'update') {
|
||||
// Update snapshot
|
||||
const dirPath = snapshotData.path.substring(0, snapshotData.path.lastIndexOf('/'));
|
||||
await smartfile.fs.ensureDir(dirPath);
|
||||
await smartfile.memory.toFs(snapshotData.content, snapshotData.path);
|
||||
if (this.logger) {
|
||||
this.logger.testConsoleOutput(`Snapshot updated: ${snapshotData.path}`);
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (this.logger) {
|
||||
this.logger.testConsoleOutput(`Error handling snapshot: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async evaluateFinalResult() {
|
||||
this.receivedTests = this.testStore.length;
|
||||
@ -219,13 +330,16 @@ export class TapParser {
|
||||
this.logger.error(`Only ${this.receivedTests} out of ${this.expectedTests} completed!`);
|
||||
}
|
||||
}
|
||||
if (!this.expectedTests) {
|
||||
if (!this.expectedTests && this.receivedTests === 0) {
|
||||
if (this.logger) {
|
||||
this.logger.error('No tests were defined. Therefore the testfile failed!');
|
||||
this.logger.testFileEnd(0, 1, 0); // Count as 1 failure
|
||||
}
|
||||
} else if (this.expectedTests !== this.receivedTests) {
|
||||
if (this.logger) {
|
||||
this.logger.error('The amount of received tests and expectedTests is unequal! Therefore the testfile failed');
|
||||
const errorCount = this.getErrorTests().length || 1; // At least 1 error
|
||||
this.logger.testFileEnd(this.receivedTests - errorCount, errorCount, 0);
|
||||
}
|
||||
} else if (this.getErrorTests().length === 0) {
|
||||
if (this.logger) {
|
||||
|
@ -99,4 +99,43 @@ export class TestDirectory {
|
||||
}
|
||||
return testFilePaths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get test files organized by parallel execution groups
|
||||
* @returns An object with grouped tests
|
||||
*/
|
||||
async getTestFileGroups(): Promise<{
|
||||
serial: string[];
|
||||
parallelGroups: { [groupName: string]: string[] };
|
||||
}> {
|
||||
await this._init();
|
||||
|
||||
const result = {
|
||||
serial: [] as string[],
|
||||
parallelGroups: {} as { [groupName: string]: string[] }
|
||||
};
|
||||
|
||||
for (const testFile of this.testfileArray) {
|
||||
const filePath = testFile.path;
|
||||
const fileName = plugins.path.basename(filePath);
|
||||
|
||||
// Check if file has parallel group pattern
|
||||
const parallelMatch = fileName.match(/\.para__(\d+)\./);
|
||||
|
||||
if (parallelMatch) {
|
||||
const groupNumber = parallelMatch[1];
|
||||
const groupName = `para__${groupNumber}`;
|
||||
|
||||
if (!result.parallelGroups[groupName]) {
|
||||
result.parallelGroups[groupName] = [];
|
||||
}
|
||||
result.parallelGroups[groupName].push(filePath);
|
||||
} else {
|
||||
// File runs serially
|
||||
result.serial.push(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
import * as plugins from './tstest.plugins.js';
|
||||
import * as paths from './tstest.paths.js';
|
||||
import * as logPrefixes from './tstest.logprefixes.js';
|
||||
|
||||
import { coloredString as cs } from '@push.rocks/consolecolor';
|
||||
|
||||
@ -15,6 +14,11 @@ export class TsTest {
|
||||
public testDir: TestDirectory;
|
||||
public executionMode: TestExecutionMode;
|
||||
public logger: TsTestLogger;
|
||||
public filterTags: string[];
|
||||
public startFromFile: number | null;
|
||||
public stopAtFile: number | null;
|
||||
public timeoutSeconds: number | null;
|
||||
private timeoutWarningTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
public smartshellInstance = new plugins.smartshell.Smartshell({
|
||||
executor: 'bash',
|
||||
@ -25,53 +29,122 @@ export class TsTest {
|
||||
|
||||
public tsbundleInstance = new plugins.tsbundle.TsBundle();
|
||||
|
||||
constructor(cwdArg: string, testPathArg: string, executionModeArg: TestExecutionMode, logOptions: LogOptions = {}) {
|
||||
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;
|
||||
this.testDir = new TestDirectory(cwdArg, testPathArg, executionModeArg);
|
||||
this.logger = new TsTestLogger(logOptions);
|
||||
this.filterTags = tags;
|
||||
this.startFromFile = startFromFile;
|
||||
this.stopAtFile = stopAtFile;
|
||||
this.timeoutSeconds = timeoutSeconds;
|
||||
}
|
||||
|
||||
async run() {
|
||||
const fileNamesToRun: string[] = await this.testDir.getTestFilePathArray();
|
||||
// Move previous log files if --logfile option is used
|
||||
if (this.logger.options.logFile) {
|
||||
await this.movePreviousLogFiles();
|
||||
}
|
||||
|
||||
// Log test discovery
|
||||
// Start timeout warning timer if no timeout was specified
|
||||
if (this.timeoutSeconds === null) {
|
||||
this.timeoutWarningTimer = setTimeout(() => {
|
||||
this.logger.warning('Test is running for more than 1 minute.');
|
||||
this.logger.warning('Consider using --timeout option to set a timeout for test files.');
|
||||
this.logger.warning('Example: tstest test --timeout=300 (for 5 minutes)');
|
||||
}, 60000); // 1 minute
|
||||
}
|
||||
|
||||
const testGroups = await this.testDir.getTestFileGroups();
|
||||
const allFiles = [...testGroups.serial, ...Object.values(testGroups.parallelGroups).flat()];
|
||||
|
||||
// Log test discovery - always show full count
|
||||
this.logger.testDiscovery(
|
||||
fileNamesToRun.length,
|
||||
allFiles.length,
|
||||
this.testDir.testPath,
|
||||
this.executionMode
|
||||
);
|
||||
|
||||
const tapCombinator = new TapCombinator(this.logger); // lets create the TapCombinator
|
||||
let fileIndex = 0;
|
||||
for (const fileNameArg of fileNamesToRun) {
|
||||
|
||||
// Execute serial tests first
|
||||
for (const fileNameArg of testGroups.serial) {
|
||||
fileIndex++;
|
||||
switch (true) {
|
||||
case process.env.CI && fileNameArg.includes('.nonci.'):
|
||||
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, fileIndex, fileNamesToRun.length);
|
||||
tapCombinator.addTapParser(tapParserBrowser);
|
||||
break;
|
||||
case fileNameArg.endsWith('.both.ts') || fileNameArg.endsWith('.both.nonci.ts'):
|
||||
this.logger.sectionStart('Part 1: Chrome');
|
||||
const tapParserBothBrowser = await this.runInChrome(fileNameArg, fileIndex, fileNamesToRun.length);
|
||||
tapCombinator.addTapParser(tapParserBothBrowser);
|
||||
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, fileIndex, fileNamesToRun.length);
|
||||
tapCombinator.addTapParser(tapParserNode);
|
||||
break;
|
||||
await this.runSingleTestOrSkip(fileNameArg, fileIndex, allFiles.length, tapCombinator);
|
||||
}
|
||||
|
||||
// Execute parallel groups sequentially
|
||||
const groupNames = Object.keys(testGroups.parallelGroups).sort();
|
||||
for (const groupName of groupNames) {
|
||||
const groupFiles = testGroups.parallelGroups[groupName];
|
||||
|
||||
if (groupFiles.length > 0) {
|
||||
this.logger.sectionStart(`Parallel Group: ${groupName}`);
|
||||
|
||||
// Run all tests in this group in parallel
|
||||
const parallelPromises = groupFiles.map(async (fileNameArg) => {
|
||||
fileIndex++;
|
||||
return this.runSingleTestOrSkip(fileNameArg, fileIndex, allFiles.length, tapCombinator);
|
||||
});
|
||||
|
||||
await Promise.all(parallelPromises);
|
||||
this.logger.sectionEnd();
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the timeout warning timer if it was set
|
||||
if (this.timeoutWarningTimer) {
|
||||
clearTimeout(this.timeoutWarningTimer);
|
||||
this.timeoutWarningTimer = null;
|
||||
}
|
||||
|
||||
tapCombinator.evaluate();
|
||||
}
|
||||
|
||||
private async runSingleTestOrSkip(fileNameArg: string, fileIndex: number, totalFiles: number, tapCombinator: TapCombinator) {
|
||||
// Check if this file should be skipped based on range
|
||||
if (this.startFromFile !== null && fileIndex < this.startFromFile) {
|
||||
this.logger.testFileSkipped(fileNameArg, fileIndex, totalFiles, `before start range (${this.startFromFile})`);
|
||||
tapCombinator.addSkippedFile(fileNameArg);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.stopAtFile !== null && fileIndex > this.stopAtFile) {
|
||||
this.logger.testFileSkipped(fileNameArg, fileIndex, totalFiles, `after stop range (${this.stopAtFile})`);
|
||||
tapCombinator.addSkippedFile(fileNameArg);
|
||||
return;
|
||||
}
|
||||
|
||||
// File is in range, run it
|
||||
await this.runSingleTest(fileNameArg, fileIndex, totalFiles, tapCombinator);
|
||||
}
|
||||
|
||||
private async runSingleTest(fileNameArg: string, fileIndex: number, totalFiles: number, tapCombinator: TapCombinator) {
|
||||
switch (true) {
|
||||
case process.env.CI && fileNameArg.includes('.nonci.'):
|
||||
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, fileIndex, totalFiles);
|
||||
tapCombinator.addTapParser(tapParserBrowser);
|
||||
break;
|
||||
case fileNameArg.endsWith('.both.ts') || fileNameArg.endsWith('.both.nonci.ts'):
|
||||
this.logger.sectionStart('Part 1: Chrome');
|
||||
const tapParserBothBrowser = await this.runInChrome(fileNameArg, fileIndex, totalFiles);
|
||||
tapCombinator.addTapParser(tapParserBothBrowser);
|
||||
this.logger.sectionEnd();
|
||||
|
||||
this.logger.sectionStart('Part 2: Node');
|
||||
const tapParserBothNode = await this.runInNode(fileNameArg, fileIndex, totalFiles);
|
||||
tapCombinator.addTapParser(tapParserBothNode);
|
||||
this.logger.sectionEnd();
|
||||
break;
|
||||
default:
|
||||
const tapParserNode = await this.runInNode(fileNameArg, fileIndex, totalFiles);
|
||||
tapCombinator.addTapParser(tapParserNode);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public async runInNode(fileNameArg: string, index: number, total: number): Promise<TapParser> {
|
||||
this.logger.testFileStart(fileNameArg, 'node.js', index, total);
|
||||
@ -82,11 +155,51 @@ export class TsTest {
|
||||
if (process.argv.includes('--web')) {
|
||||
tsrunOptions += ' --web';
|
||||
}
|
||||
|
||||
// Set filter tags as environment variable
|
||||
if (this.filterTags.length > 0) {
|
||||
process.env.TSTEST_FILTER_TAGS = this.filterTags.join(',');
|
||||
}
|
||||
|
||||
const execResultStreaming = await this.smartshellInstance.execStreamingSilent(
|
||||
`tsrun ${fileNameArg}${tsrunOptions}`
|
||||
);
|
||||
await tapParser.handleTapProcess(execResultStreaming.childProcess);
|
||||
|
||||
// Handle timeout if specified
|
||||
if (this.timeoutSeconds !== null) {
|
||||
const timeoutMs = this.timeoutSeconds * 1000;
|
||||
let timeoutId: NodeJS.Timeout;
|
||||
|
||||
const timeoutPromise = new Promise<void>((_resolve, reject) => {
|
||||
timeoutId = setTimeout(async () => {
|
||||
// Use smartshell's terminate() to kill entire process tree
|
||||
await execResultStreaming.terminate();
|
||||
reject(new Error(`Test file timed out after ${this.timeoutSeconds} seconds`));
|
||||
}, timeoutMs);
|
||||
});
|
||||
|
||||
try {
|
||||
await Promise.race([
|
||||
tapParser.handleTapProcess(execResultStreaming.childProcess),
|
||||
timeoutPromise
|
||||
]);
|
||||
// Clear timeout if test completed successfully
|
||||
clearTimeout(timeoutId);
|
||||
} catch (error) {
|
||||
// Handle timeout error
|
||||
tapParser.handleTimeout(this.timeoutSeconds);
|
||||
// Ensure entire process tree is killed if still running
|
||||
try {
|
||||
await execResultStreaming.kill(); // This kills the entire process tree with SIGKILL
|
||||
} catch (killError) {
|
||||
// Process tree might already be dead
|
||||
}
|
||||
await tapParser.evaluateFinalResult();
|
||||
}
|
||||
} else {
|
||||
await tapParser.handleTapProcess(execResultStreaming.childProcess);
|
||||
}
|
||||
|
||||
return tapParser;
|
||||
}
|
||||
|
||||
@ -111,7 +224,7 @@ export class TsTest {
|
||||
});
|
||||
server.addRoute(
|
||||
'/test',
|
||||
new plugins.typedserver.servertools.Handler('GET', async (req, res) => {
|
||||
new plugins.typedserver.servertools.Handler('GET', async (_req, res) => {
|
||||
res.type('.html');
|
||||
res.write(`
|
||||
<html>
|
||||
@ -144,9 +257,10 @@ export class TsTest {
|
||||
});
|
||||
});
|
||||
|
||||
// lets do the browser bit
|
||||
// lets do the browser bit with timeout handling
|
||||
await this.smartbrowserInstance.start();
|
||||
const evaluation = await this.smartbrowserInstance.evaluateOnPage(
|
||||
|
||||
const evaluatePromise = this.smartbrowserInstance.evaluateOnPage(
|
||||
`http://localhost:3007/test?bundleName=${bundleFileName}`,
|
||||
async () => {
|
||||
// lets enable real time comms
|
||||
@ -159,12 +273,12 @@ export class TsTest {
|
||||
const originalError = console.error;
|
||||
|
||||
// Override console methods to capture the logs
|
||||
console.log = (...args) => {
|
||||
console.log = (...args: any[]) => {
|
||||
logStore.push(args.join(' '));
|
||||
ws.send(args.join(' '));
|
||||
originalLog(...args);
|
||||
};
|
||||
console.error = (...args) => {
|
||||
console.error = (...args: any[]) => {
|
||||
logStore.push(args.join(' '));
|
||||
ws.send(args.join(' '));
|
||||
originalError(...args);
|
||||
@ -203,16 +317,105 @@ export class TsTest {
|
||||
return logStore.join('\n');
|
||||
}
|
||||
);
|
||||
await this.smartbrowserInstance.stop();
|
||||
await server.stop();
|
||||
wss.close();
|
||||
|
||||
// Handle timeout if specified
|
||||
if (this.timeoutSeconds !== null) {
|
||||
const timeoutMs = this.timeoutSeconds * 1000;
|
||||
let timeoutId: NodeJS.Timeout;
|
||||
|
||||
const timeoutPromise = new Promise<void>((_resolve, reject) => {
|
||||
timeoutId = setTimeout(() => {
|
||||
reject(new Error(`Test file timed out after ${this.timeoutSeconds} seconds`));
|
||||
}, timeoutMs);
|
||||
});
|
||||
|
||||
try {
|
||||
await Promise.race([
|
||||
evaluatePromise,
|
||||
timeoutPromise
|
||||
]);
|
||||
// Clear timeout if test completed successfully
|
||||
clearTimeout(timeoutId);
|
||||
} catch (error) {
|
||||
// Handle timeout error
|
||||
tapParser.handleTimeout(this.timeoutSeconds);
|
||||
}
|
||||
} else {
|
||||
await evaluatePromise;
|
||||
}
|
||||
|
||||
// Always clean up resources, even on timeout
|
||||
try {
|
||||
await this.smartbrowserInstance.stop();
|
||||
} catch (error) {
|
||||
// Browser might already be stopped
|
||||
}
|
||||
|
||||
try {
|
||||
await server.stop();
|
||||
} catch (error) {
|
||||
// Server might already be stopped
|
||||
}
|
||||
|
||||
try {
|
||||
wss.close();
|
||||
} catch (error) {
|
||||
// WebSocket server might already be closed
|
||||
}
|
||||
|
||||
console.log(
|
||||
`${cs('=> ', 'blue')} Stopped ${cs(fileNameArg, 'orange')} chromium instance and server.`
|
||||
);
|
||||
// lets create the tap parser
|
||||
// Always evaluate final result (handleTimeout just sets up the test state)
|
||||
await tapParser.evaluateFinalResult();
|
||||
return tapParser;
|
||||
}
|
||||
|
||||
public async runInDeno() {}
|
||||
|
||||
private async movePreviousLogFiles() {
|
||||
const logDir = plugins.path.join('.nogit', 'testlogs');
|
||||
const previousDir = plugins.path.join('.nogit', 'testlogs', 'previous');
|
||||
const errDir = plugins.path.join('.nogit', 'testlogs', '00err');
|
||||
const diffDir = plugins.path.join('.nogit', 'testlogs', '00diff');
|
||||
|
||||
try {
|
||||
// Delete 00err and 00diff directories if they exist
|
||||
if (await plugins.smartfile.fs.isDirectory(errDir)) {
|
||||
await plugins.smartfile.fs.remove(errDir);
|
||||
}
|
||||
if (await plugins.smartfile.fs.isDirectory(diffDir)) {
|
||||
await plugins.smartfile.fs.remove(diffDir);
|
||||
}
|
||||
|
||||
// Get all .log files in log directory (not in subdirectories)
|
||||
const files = await plugins.smartfile.fs.listFileTree(logDir, '*.log');
|
||||
const logFiles = files.filter((file: string) => !file.includes('/'));
|
||||
|
||||
if (logFiles.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure previous directory exists
|
||||
await plugins.smartfile.fs.ensureDir(previousDir);
|
||||
|
||||
// Move each log file to previous directory
|
||||
for (const file of logFiles) {
|
||||
const filename = plugins.path.basename(file);
|
||||
const sourcePath = plugins.path.join(logDir, filename);
|
||||
const destPath = plugins.path.join(previousDir, filename);
|
||||
|
||||
try {
|
||||
// Copy file to new location and remove original
|
||||
await plugins.smartfile.fs.copy(sourcePath, destPath);
|
||||
await plugins.smartfile.fs.remove(sourcePath);
|
||||
} catch (error) {
|
||||
// Silently continue if a file can't be moved
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Directory might not exist, which is fine
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -30,12 +30,14 @@ export interface TestSummary {
|
||||
totalTests: number;
|
||||
totalPassed: number;
|
||||
totalFailed: number;
|
||||
totalSkipped: number;
|
||||
totalDuration: number;
|
||||
fileResults: TestFileResult[];
|
||||
skippedFiles: string[];
|
||||
}
|
||||
|
||||
export class TsTestLogger {
|
||||
private options: LogOptions;
|
||||
public readonly options: LogOptions;
|
||||
private startTime: number;
|
||||
private fileResults: TestFileResult[] = [];
|
||||
private currentFileResult: TestFileResult | null = null;
|
||||
@ -153,8 +155,16 @@ export class TsTestLogger {
|
||||
|
||||
// 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`);
|
||||
// Create a safe filename that preserves directory structure
|
||||
// Convert relative path to a flat filename by replacing separators with __
|
||||
const relativeFilename = path.relative(process.cwd(), filename);
|
||||
const safeFilename = relativeFilename
|
||||
.replace(/\\/g, '/') // Normalize Windows paths
|
||||
.replace(/\//g, '__') // Replace path separators with double underscores
|
||||
.replace(/\.ts$/, '') // Remove .ts extension
|
||||
.replace(/^\.\.__|^\.__|^__/, ''); // Clean up leading separators from relative paths
|
||||
|
||||
this.currentTestLogFile = path.join('.nogit', 'testlogs', `${safeFilename}.log`);
|
||||
|
||||
// Ensure the directory exists
|
||||
const logDir = path.dirname(this.currentTestLogFile);
|
||||
@ -237,6 +247,44 @@ export class TsTestLogger {
|
||||
this.log(this.format(` Summary: ${passed}/${total} ${status}`, color));
|
||||
}
|
||||
|
||||
// If using --logfile, handle error copy and diff detection
|
||||
if (this.options.logFile && this.currentTestLogFile) {
|
||||
try {
|
||||
const logContent = fs.readFileSync(this.currentTestLogFile, 'utf-8');
|
||||
const logDir = path.dirname(this.currentTestLogFile);
|
||||
const logBasename = path.basename(this.currentTestLogFile);
|
||||
|
||||
// Create error copy if there were failures
|
||||
if (failed > 0) {
|
||||
const errorDir = path.join(logDir, '00err');
|
||||
if (!fs.existsSync(errorDir)) {
|
||||
fs.mkdirSync(errorDir, { recursive: true });
|
||||
}
|
||||
const errorLogPath = path.join(errorDir, logBasename);
|
||||
fs.writeFileSync(errorLogPath, logContent);
|
||||
}
|
||||
|
||||
// Check for previous version and create diff if changed
|
||||
const previousLogPath = path.join(logDir, 'previous', logBasename);
|
||||
if (fs.existsSync(previousLogPath)) {
|
||||
const previousContent = fs.readFileSync(previousLogPath, 'utf-8');
|
||||
|
||||
// Simple check if content differs
|
||||
if (previousContent !== logContent) {
|
||||
const diffDir = path.join(logDir, '00diff');
|
||||
if (!fs.existsSync(diffDir)) {
|
||||
fs.mkdirSync(diffDir, { recursive: true });
|
||||
}
|
||||
const diffLogPath = path.join(diffDir, logBasename);
|
||||
const diffContent = this.createDiff(previousContent, logContent, logBasename);
|
||||
fs.writeFileSync(diffLogPath, diffContent);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Silently fail to avoid disrupting the test run
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the current test log file reference only if using --logfile
|
||||
if (this.options.logFile) {
|
||||
this.currentTestLogFile = null;
|
||||
@ -244,7 +292,7 @@ export class TsTestLogger {
|
||||
}
|
||||
|
||||
// TAP output forwarding (for TAP protocol messages)
|
||||
tapOutput(message: string, isError: boolean = false) {
|
||||
tapOutput(message: string, _isError: boolean = false) {
|
||||
if (this.options.json) return;
|
||||
|
||||
// Never show raw TAP protocol messages in console
|
||||
@ -274,6 +322,19 @@ export class TsTestLogger {
|
||||
}
|
||||
}
|
||||
|
||||
// Skipped test file
|
||||
testFileSkipped(filename: string, index: number, total: number, reason: string) {
|
||||
if (this.options.json) {
|
||||
this.logJson({ event: 'fileSkipped', filename, index, total, reason });
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.options.quiet) return;
|
||||
|
||||
this.log(this.format(`\n⏭️ ${filename} (${index}/${total})`, 'yellow'));
|
||||
this.log(this.format(` Skipped: ${reason}`, 'dim'));
|
||||
}
|
||||
|
||||
// Browser console
|
||||
browserConsole(message: string, level: string = 'log') {
|
||||
if (this.options.json) {
|
||||
@ -309,15 +370,17 @@ export class TsTestLogger {
|
||||
}
|
||||
|
||||
// Final summary
|
||||
summary() {
|
||||
summary(skippedFiles: string[] = []) {
|
||||
const totalDuration = Date.now() - this.startTime;
|
||||
const summary: TestSummary = {
|
||||
totalFiles: this.fileResults.length,
|
||||
totalFiles: this.fileResults.length + skippedFiles.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),
|
||||
totalSkipped: skippedFiles.length,
|
||||
totalDuration,
|
||||
fileResults: this.fileResults
|
||||
fileResults: this.fileResults,
|
||||
skippedFiles
|
||||
};
|
||||
|
||||
if (this.options.json) {
|
||||
@ -338,6 +401,9 @@ export class TsTestLogger {
|
||||
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'));
|
||||
if (summary.totalSkipped > 0) {
|
||||
this.log(this.format(`│ Skipped: ${summary.totalSkipped.toString().padStart(14)} │`, 'yellow'));
|
||||
}
|
||||
this.log(this.format(`│ Duration: ${totalDuration.toString().padStart(14)}ms │`, 'white'));
|
||||
this.log(this.format('└────────────────────────────────┘', 'dim'));
|
||||
|
||||
@ -377,6 +443,20 @@ export class TsTestLogger {
|
||||
this.log(this.format(`\n${status}`, statusColor));
|
||||
}
|
||||
|
||||
// Warning display
|
||||
warning(message: string) {
|
||||
if (this.options.json) {
|
||||
this.logJson({ event: 'warning', message });
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.options.quiet) {
|
||||
console.log(`WARNING: ${message}`);
|
||||
} else {
|
||||
this.log(this.format(` ⚠️ ${message}`, 'orange'));
|
||||
}
|
||||
}
|
||||
|
||||
// Error display
|
||||
error(message: string, file?: string, stack?: string) {
|
||||
if (this.options.json) {
|
||||
@ -396,4 +476,48 @@ export class TsTestLogger {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a diff between two log contents
|
||||
private createDiff(previousContent: string, currentContent: string, filename: string): string {
|
||||
const previousLines = previousContent.split('\n');
|
||||
const currentLines = currentContent.split('\n');
|
||||
|
||||
let diff = `DIFF REPORT: ${filename}\n`;
|
||||
diff += `Generated: ${new Date().toISOString()}\n`;
|
||||
diff += '='.repeat(80) + '\n\n';
|
||||
|
||||
// Simple line-by-line comparison
|
||||
const maxLines = Math.max(previousLines.length, currentLines.length);
|
||||
let hasChanges = false;
|
||||
|
||||
for (let i = 0; i < maxLines; i++) {
|
||||
const prevLine = previousLines[i] || '';
|
||||
const currLine = currentLines[i] || '';
|
||||
|
||||
if (prevLine !== currLine) {
|
||||
hasChanges = true;
|
||||
if (i < previousLines.length && i >= currentLines.length) {
|
||||
// Line was removed
|
||||
diff += `- [Line ${i + 1}] ${prevLine}\n`;
|
||||
} else if (i >= previousLines.length && i < currentLines.length) {
|
||||
// Line was added
|
||||
diff += `+ [Line ${i + 1}] ${currLine}\n`;
|
||||
} else {
|
||||
// Line was modified
|
||||
diff += `- [Line ${i + 1}] ${prevLine}\n`;
|
||||
diff += `+ [Line ${i + 1}] ${currLine}\n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasChanges) {
|
||||
diff += 'No changes detected.\n';
|
||||
}
|
||||
|
||||
diff += '\n' + '='.repeat(80) + '\n';
|
||||
diff += `Previous version had ${previousLines.length} lines\n`;
|
||||
diff += `Current version has ${currentLines.length} lines\n`;
|
||||
|
||||
return diff;
|
||||
}
|
||||
}
|
@ -2,6 +2,10 @@ export { tap } from './tapbundle.classes.tap.js';
|
||||
export { TapWrap } from './tapbundle.classes.tapwrap.js';
|
||||
export { webhelpers } from './webhelpers.js';
|
||||
|
||||
// Protocol utilities (for future protocol v2)
|
||||
export * from './tapbundle.protocols.js';
|
||||
export { TapTools } from './tapbundle.classes.taptools.js';
|
||||
|
||||
import { expect } from '@push.rocks/smartexpect';
|
||||
|
||||
export { expect };
|
||||
|
@ -2,7 +2,137 @@ 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 interface ITestSuite {
|
||||
description: string;
|
||||
tests: TapTest<any>[];
|
||||
beforeEach?: ITestFunction<any>;
|
||||
afterEach?: ITestFunction<any>;
|
||||
parent?: ITestSuite;
|
||||
children: ITestSuite[];
|
||||
}
|
||||
|
||||
class TestBuilder<T> {
|
||||
private _tap: Tap<T>;
|
||||
private _tags: string[] = [];
|
||||
private _priority: 'high' | 'medium' | 'low' = 'medium';
|
||||
private _retryCount?: number;
|
||||
private _timeoutMs?: number;
|
||||
|
||||
constructor(tap: Tap<T>) {
|
||||
this._tap = tap;
|
||||
}
|
||||
|
||||
tags(...tags: string[]) {
|
||||
this._tags = tags;
|
||||
return this;
|
||||
}
|
||||
|
||||
priority(level: 'high' | 'medium' | 'low') {
|
||||
this._priority = level;
|
||||
return this;
|
||||
}
|
||||
|
||||
retry(count: number) {
|
||||
this._retryCount = count;
|
||||
return this;
|
||||
}
|
||||
|
||||
timeout(ms: number) {
|
||||
this._timeoutMs = ms;
|
||||
return this;
|
||||
}
|
||||
|
||||
test(description: string, testFunction: ITestFunction<T>) {
|
||||
const test = this._tap.test(description, testFunction, 'normal');
|
||||
|
||||
// Apply settings to the test
|
||||
if (this._tags.length > 0) {
|
||||
test.tags = this._tags;
|
||||
}
|
||||
test.priority = this._priority;
|
||||
|
||||
if (this._retryCount !== undefined) {
|
||||
test.tapTools.retry(this._retryCount);
|
||||
}
|
||||
if (this._timeoutMs !== undefined) {
|
||||
test.timeoutMs = this._timeoutMs;
|
||||
}
|
||||
|
||||
return test;
|
||||
}
|
||||
|
||||
testOnly(description: string, testFunction: ITestFunction<T>) {
|
||||
const test = this._tap.test(description, testFunction, 'only');
|
||||
|
||||
// Apply settings to the test
|
||||
if (this._tags.length > 0) {
|
||||
test.tags = this._tags;
|
||||
}
|
||||
test.priority = this._priority;
|
||||
|
||||
if (this._retryCount !== undefined) {
|
||||
test.tapTools.retry(this._retryCount);
|
||||
}
|
||||
if (this._timeoutMs !== undefined) {
|
||||
test.timeoutMs = this._timeoutMs;
|
||||
}
|
||||
|
||||
return test;
|
||||
}
|
||||
|
||||
testSkip(description: string, testFunction: ITestFunction<T>) {
|
||||
const test = this._tap.test(description, testFunction, 'skip');
|
||||
|
||||
// Apply settings to the test
|
||||
if (this._tags.length > 0) {
|
||||
test.tags = this._tags;
|
||||
}
|
||||
test.priority = this._priority;
|
||||
|
||||
if (this._retryCount !== undefined) {
|
||||
test.tapTools.retry(this._retryCount);
|
||||
}
|
||||
if (this._timeoutMs !== undefined) {
|
||||
test.timeoutMs = this._timeoutMs;
|
||||
}
|
||||
|
||||
return test;
|
||||
}
|
||||
}
|
||||
|
||||
export class Tap<T> {
|
||||
private _skipCount = 0;
|
||||
private _filterTags: string[] = [];
|
||||
|
||||
constructor() {
|
||||
// Get filter tags from environment
|
||||
if (typeof process !== 'undefined' && process.env && process.env.TSTEST_FILTER_TAGS) {
|
||||
this._filterTags = process.env.TSTEST_FILTER_TAGS.split(',');
|
||||
}
|
||||
}
|
||||
|
||||
// Fluent test builder
|
||||
public tags(...tags: string[]) {
|
||||
const builder = new TestBuilder<T>(this);
|
||||
return builder.tags(...tags);
|
||||
}
|
||||
|
||||
public priority(level: 'high' | 'medium' | 'low') {
|
||||
const builder = new TestBuilder<T>(this);
|
||||
return builder.priority(level);
|
||||
}
|
||||
|
||||
public retry(count: number) {
|
||||
const builder = new TestBuilder<T>(this);
|
||||
return builder.retry(count);
|
||||
}
|
||||
|
||||
public timeout(ms: number) {
|
||||
const builder = new TestBuilder<T>(this);
|
||||
return builder.timeout(ms);
|
||||
}
|
||||
|
||||
/**
|
||||
* skips a test
|
||||
* tests marked with tap.skip.test() are never executed
|
||||
@ -10,9 +140,11 @@ export class Tap<T> {
|
||||
public skip = {
|
||||
test: (descriptionArg: string, functionArg: ITestFunction<T>) => {
|
||||
console.log(`skipped test: ${descriptionArg}`);
|
||||
this._skipCount++;
|
||||
},
|
||||
testParallel: (descriptionArg: string, functionArg: ITestFunction<T>) => {
|
||||
console.log(`skipped test: ${descriptionArg}`);
|
||||
this._skipCount++;
|
||||
},
|
||||
};
|
||||
|
||||
@ -28,6 +160,8 @@ export class Tap<T> {
|
||||
private _tapPreTasks: PreTask[] = [];
|
||||
private _tapTests: TapTest<any>[] = [];
|
||||
private _tapTestsOnly: TapTest<any>[] = [];
|
||||
private _currentSuite: ITestSuite | null = null;
|
||||
private _rootSuites: ITestSuite[] = [];
|
||||
|
||||
/**
|
||||
* Normal test function, will run one by one
|
||||
@ -37,17 +171,26 @@ export class Tap<T> {
|
||||
public test(
|
||||
testDescription: string,
|
||||
testFunction: ITestFunction<T>,
|
||||
modeArg: 'normal' | 'only' | 'skip' = 'normal',
|
||||
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);
|
||||
|
||||
// No options applied here - use the fluent builder syntax instead
|
||||
|
||||
// If we're in a suite, add test to the suite
|
||||
if (this._currentSuite) {
|
||||
this._currentSuite.tests.push(localTest);
|
||||
} else {
|
||||
// Otherwise add to global test list
|
||||
if (modeArg === 'normal') {
|
||||
this._tapTests.push(localTest);
|
||||
} else if (modeArg === 'only') {
|
||||
this._tapTestsOnly.push(localTest);
|
||||
}
|
||||
}
|
||||
return localTest;
|
||||
}
|
||||
@ -62,32 +205,109 @@ export class Tap<T> {
|
||||
* @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,
|
||||
}),
|
||||
);
|
||||
const localTest = new TapTest({
|
||||
description: testDescription,
|
||||
testFunction,
|
||||
parallel: true,
|
||||
});
|
||||
|
||||
if (this._currentSuite) {
|
||||
this._currentSuite.tests.push(localTest);
|
||||
} else {
|
||||
this._tapTests.push(localTest);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a test suite for grouping related tests
|
||||
*/
|
||||
public describe(description: string, suiteFunction: () => void) {
|
||||
const suite: ITestSuite = {
|
||||
description,
|
||||
tests: [],
|
||||
children: [],
|
||||
parent: this._currentSuite,
|
||||
};
|
||||
|
||||
// Add to parent or root
|
||||
if (this._currentSuite) {
|
||||
this._currentSuite.children.push(suite);
|
||||
} else {
|
||||
this._rootSuites.push(suite);
|
||||
}
|
||||
|
||||
// Execute suite function in context
|
||||
const previousSuite = this._currentSuite;
|
||||
this._currentSuite = suite;
|
||||
try {
|
||||
suiteFunction();
|
||||
} finally {
|
||||
this._currentSuite = previousSuite;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up a function to run before each test in the current suite
|
||||
*/
|
||||
public beforeEach(setupFunction: ITestFunction<any>) {
|
||||
if (this._currentSuite) {
|
||||
this._currentSuite.beforeEach = setupFunction;
|
||||
} else {
|
||||
throw new Error('beforeEach can only be used inside a describe block');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up a function to run after each test in the current suite
|
||||
*/
|
||||
public afterEach(teardownFunction: ITestFunction<any>) {
|
||||
if (this._currentSuite) {
|
||||
this._currentSuite.afterEach = teardownFunction;
|
||||
} else {
|
||||
throw new Error('afterEach can only be used inside a describe block');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* collect all tests from suites
|
||||
*/
|
||||
private _collectTests(suite: ITestSuite, tests: TapTest<any>[] = []): TapTest<any>[] {
|
||||
tests.push(...suite.tests);
|
||||
for (const childSuite of suite.children) {
|
||||
this._collectTests(childSuite, tests);
|
||||
}
|
||||
return tests;
|
||||
}
|
||||
|
||||
/**
|
||||
* starts the test evaluation
|
||||
*/
|
||||
public async start(optionsArg?: { throwOnError: boolean }) {
|
||||
// lets set the tapbundle promise
|
||||
const smartenvInstance = new plugins.smartenv.Smartenv();
|
||||
const globalPromise = plugins.smartpromise.defer();
|
||||
smartenvInstance.isBrowser
|
||||
? ((globalThis as any).tapbundleDeferred = plugins.smartpromise.defer())
|
||||
? ((globalThis as any).tapbundleDeferred = globalPromise)
|
||||
: null;
|
||||
// Also set tapPromise for backwards compatibility
|
||||
smartenvInstance.isBrowser
|
||||
? ((globalThis as any).tapPromise = globalPromise.promise)
|
||||
: null;
|
||||
|
||||
// Path helpers will be initialized by the Node.js environment if available
|
||||
|
||||
// lets continue with running the tests
|
||||
const promiseArray: Array<Promise<any>> = [];
|
||||
|
||||
// Collect all tests including those in suites
|
||||
let allTests: TapTest<any>[] = [...this._tapTests];
|
||||
for (const suite of this._rootSuites) {
|
||||
this._collectTests(suite, allTests);
|
||||
}
|
||||
|
||||
// safeguard against empty test array
|
||||
if (this._tapTests.length === 0) {
|
||||
if (allTests.length === 0 && this._tapTestsOnly.length === 0) {
|
||||
console.log('no tests specified. Ending here!');
|
||||
// TODO: throw proper error
|
||||
return;
|
||||
}
|
||||
|
||||
@ -96,7 +316,19 @@ export class Tap<T> {
|
||||
if (this._tapTestsOnly.length > 0) {
|
||||
concerningTests = this._tapTestsOnly;
|
||||
} else {
|
||||
concerningTests = this._tapTests;
|
||||
concerningTests = allTests;
|
||||
}
|
||||
|
||||
// Filter tests by tags if specified
|
||||
if (this._filterTags.length > 0) {
|
||||
concerningTests = concerningTests.filter(test => {
|
||||
// Skip tests without tags when filtering is active
|
||||
if (!test.tags || test.tags.length === 0) {
|
||||
return false;
|
||||
}
|
||||
// Check if test has any of the filter tags
|
||||
return test.tags.some(tag => this._filterTags.includes(tag));
|
||||
});
|
||||
}
|
||||
|
||||
// lets run the pretasks
|
||||
@ -104,16 +336,43 @@ export class Tap<T> {
|
||||
await preTask.run();
|
||||
}
|
||||
|
||||
// Count actual tests that will be run
|
||||
console.log(`1..${concerningTests.length}`);
|
||||
for (let testKey = 0; testKey < concerningTests.length; testKey++) {
|
||||
const currentTest = concerningTests[testKey];
|
||||
const testPromise = currentTest.run(testKey);
|
||||
|
||||
// Run tests from suites with lifecycle hooks
|
||||
let testKey = 0;
|
||||
|
||||
// Run root suite tests with lifecycle hooks
|
||||
if (this._rootSuites.length > 0) {
|
||||
await this._runSuite(null, this._rootSuites, promiseArray, { testKey });
|
||||
// Update testKey after running suite tests
|
||||
for (const suite of this._rootSuites) {
|
||||
const suiteTests = this._collectTests(suite);
|
||||
testKey += suiteTests.length;
|
||||
}
|
||||
}
|
||||
|
||||
// Run non-suite tests (tests added directly without describe)
|
||||
const nonSuiteTests = concerningTests.filter(test => {
|
||||
// Check if test is not in any suite
|
||||
for (const suite of this._rootSuites) {
|
||||
const suiteTests = this._collectTests(suite);
|
||||
if (suiteTests.includes(test)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
for (const currentTest of nonSuiteTests) {
|
||||
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
|
||||
@ -121,7 +380,7 @@ export class Tap<T> {
|
||||
const executionNotes: string[] = [];
|
||||
// collect failed tests
|
||||
for (const tapTest of concerningTests) {
|
||||
if (tapTest.status !== 'success') {
|
||||
if (tapTest.status !== 'success' && tapTest.status !== 'skipped') {
|
||||
failReasons.push(
|
||||
`Test ${tapTest.testKey + 1} failed with status ${tapTest.status}:\n` +
|
||||
`|| ${tapTest.description}\n` +
|
||||
@ -136,21 +395,86 @@ export class Tap<T> {
|
||||
}
|
||||
|
||||
if (optionsArg && optionsArg.throwOnError && failReasons.length > 0) {
|
||||
if (!smartenvInstance.isBrowser) process.exit(1);
|
||||
if (!smartenvInstance.isBrowser && typeof process !== 'undefined') process.exit(1);
|
||||
}
|
||||
if (smartenvInstance.isBrowser) {
|
||||
(globalThis as any).tapbundleDeferred.resolve();
|
||||
globalPromise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run tests in a suite with lifecycle hooks
|
||||
*/
|
||||
private async _runSuite(
|
||||
parentSuite: ITestSuite | null,
|
||||
suites: ITestSuite[],
|
||||
promiseArray: Promise<any>[],
|
||||
context: { testKey: number }
|
||||
) {
|
||||
for (const suite of suites) {
|
||||
// Run beforeEach from parent suites
|
||||
const beforeEachFunctions: ITestFunction<any>[] = [];
|
||||
let currentSuite: ITestSuite | null = suite;
|
||||
while (currentSuite) {
|
||||
if (currentSuite.beforeEach) {
|
||||
beforeEachFunctions.unshift(currentSuite.beforeEach);
|
||||
}
|
||||
currentSuite = currentSuite.parent || null;
|
||||
}
|
||||
|
||||
// Run tests in this suite
|
||||
for (const test of suite.tests) {
|
||||
// Create wrapper test function that includes lifecycle hooks
|
||||
const originalFunction = test.testFunction;
|
||||
test.testFunction = async (tapTools) => {
|
||||
// Run all beforeEach hooks
|
||||
for (const beforeEach of beforeEachFunctions) {
|
||||
await beforeEach(tapTools);
|
||||
}
|
||||
|
||||
// Run the actual test
|
||||
const result = await originalFunction(tapTools);
|
||||
|
||||
// Run afterEach hooks in reverse order
|
||||
const afterEachFunctions: ITestFunction<any>[] = [];
|
||||
currentSuite = suite;
|
||||
while (currentSuite) {
|
||||
if (currentSuite.afterEach) {
|
||||
afterEachFunctions.push(currentSuite.afterEach);
|
||||
}
|
||||
currentSuite = currentSuite.parent || null;
|
||||
}
|
||||
|
||||
for (const afterEach of afterEachFunctions) {
|
||||
await afterEach(tapTools);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const testPromise = test.run(context.testKey++);
|
||||
if (test.parallel) {
|
||||
promiseArray.push(testPromise);
|
||||
} else {
|
||||
await testPromise;
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively run child suites
|
||||
await this._runSuite(suite, suite.children, promiseArray, context);
|
||||
}
|
||||
}
|
||||
|
||||
public async stopForcefully(codeArg = 0, directArg = false) {
|
||||
console.log(`tap stopping forcefully! Code: ${codeArg} / Direct: ${directArg}`);
|
||||
if (directArg) {
|
||||
process.exit(codeArg);
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
if (typeof process !== 'undefined') {
|
||||
if (directArg) {
|
||||
process.exit(codeArg);
|
||||
}, 10);
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
process.exit(codeArg);
|
||||
}, 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -170,4 +494,4 @@ export class Tap<T> {
|
||||
}
|
||||
}
|
||||
|
||||
export let tap = new Tap();
|
||||
export const tap = new Tap();
|
||||
|
@ -1,13 +1,13 @@
|
||||
import * as plugins from './tapbundle.plugins.js';
|
||||
import { tapCreator } from './tapbundle.tapcreator.js';
|
||||
import { TapTools } from './tapbundle.classes.taptools.js';
|
||||
import { TapTools, SkipError } 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 type TTestStatus = 'success' | 'error' | 'pending' | 'errorAfterSuccess' | 'timeout' | 'skipped';
|
||||
|
||||
export interface ITestFunction<T> {
|
||||
(tapTools?: TapTools): Promise<T>;
|
||||
@ -22,6 +22,12 @@ export class TapTest<T = unknown> {
|
||||
public tapTools: TapTools;
|
||||
public testFunction: ITestFunction<T>;
|
||||
public testKey: number; // the testKey the position in the test qeue. Set upon calling .run()
|
||||
public timeoutMs?: number;
|
||||
public isTodo: boolean = false;
|
||||
public todoReason?: string;
|
||||
public tags: string[] = [];
|
||||
public priority: 'high' | 'medium' | 'low' = 'medium';
|
||||
public fileName?: string;
|
||||
private testDeferred: Deferred<TapTest<T>> = plugins.smartpromise.defer();
|
||||
public testPromise: Promise<TapTest<T>> = this.testDeferred.promise;
|
||||
private testResultDeferred: Deferred<T> = plugins.smartpromise.defer();
|
||||
@ -46,42 +52,102 @@ export class TapTest<T = unknown> {
|
||||
* 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`,
|
||||
);
|
||||
|
||||
// Handle todo tests
|
||||
if (this.isTodo) {
|
||||
const todoText = this.todoReason ? `# TODO ${this.todoReason}` : '# TODO';
|
||||
console.log(`ok ${testNumber} - ${this.description} ${todoText}`);
|
||||
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';
|
||||
return;
|
||||
}
|
||||
|
||||
// Run test with retries
|
||||
let lastError: any;
|
||||
const maxRetries = this.tapTools.maxRetries;
|
||||
|
||||
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
||||
this.hrtMeasurement.start();
|
||||
|
||||
try {
|
||||
// Set up timeout if specified
|
||||
let timeoutHandle: any;
|
||||
let timeoutPromise: Promise<never> | null = null;
|
||||
|
||||
if (this.timeoutMs) {
|
||||
timeoutPromise = new Promise<never>((_, reject) => {
|
||||
timeoutHandle = setTimeout(() => {
|
||||
this.status = 'timeout';
|
||||
reject(new Error(`Test timed out after ${this.timeoutMs}ms`));
|
||||
}, this.timeoutMs);
|
||||
});
|
||||
}
|
||||
|
||||
// Run the test function with potential timeout
|
||||
const testPromise = this.testFunction(this.tapTools);
|
||||
const testReturnValue = timeoutPromise
|
||||
? await Promise.race([testPromise, timeoutPromise])
|
||||
: await testPromise;
|
||||
|
||||
// Clear timeout if test completed
|
||||
if (timeoutHandle) {
|
||||
clearTimeout(timeoutHandle);
|
||||
}
|
||||
|
||||
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);
|
||||
return; // Success, exit retry loop
|
||||
|
||||
} catch (err: any) {
|
||||
this.hrtMeasurement.stop();
|
||||
|
||||
// Handle skip
|
||||
if (err instanceof SkipError || err.name === 'SkipError') {
|
||||
console.log(`ok ${testNumber} - ${this.description} # SKIP ${err.message.replace('Skipped: ', '')}`);
|
||||
this.status = 'skipped';
|
||||
this.testDeferred.resolve(this);
|
||||
return;
|
||||
}
|
||||
|
||||
lastError = err;
|
||||
|
||||
// If we have retries left, try again
|
||||
if (attempt < maxRetries) {
|
||||
console.log(
|
||||
`# Retry ${attempt + 1}/${maxRetries} for test: ${this.description}`,
|
||||
);
|
||||
this.tapTools._incrementRetryCount();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Final failure
|
||||
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);
|
||||
}
|
||||
|
||||
// if the test is allowed to fail
|
||||
if (this.failureAllowed) {
|
||||
console.log(`please note: failure allowed!`);
|
||||
}
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,14 +5,33 @@ export interface IPromiseFunc {
|
||||
(): Promise<any>;
|
||||
}
|
||||
|
||||
export class SkipError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'SkipError';
|
||||
}
|
||||
}
|
||||
|
||||
export class TapTools {
|
||||
/**
|
||||
* the referenced TapTest
|
||||
*/
|
||||
private _tapTest: TapTest;
|
||||
private _retries = 0;
|
||||
private _retryCount = 0;
|
||||
public testData: any = {};
|
||||
private static _sharedContext = new Map<string, any>();
|
||||
private _snapshotPath: string = '';
|
||||
|
||||
constructor(TapTestArg: TapTest<any>) {
|
||||
this._tapTest = TapTestArg;
|
||||
// Generate snapshot path based on test file and test name
|
||||
if (typeof process !== 'undefined' && process.cwd && TapTestArg) {
|
||||
const testFile = TapTestArg.fileName || 'unknown';
|
||||
const testName = TapTestArg.description.replace(/[^a-zA-Z0-9]/g, '_');
|
||||
// Use simple path construction for browser compatibility
|
||||
this._snapshotPath = `${process.cwd()}/.nogit/test_snapshots/${testFile}/${testName}.snap`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -22,6 +41,59 @@ export class TapTools {
|
||||
this._tapTest.failureAllowed = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* skip the rest of the test
|
||||
*/
|
||||
public skip(reason?: string): never {
|
||||
const skipMessage = reason ? `Skipped: ${reason}` : 'Skipped';
|
||||
throw new SkipError(skipMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* conditionally skip the rest of the test
|
||||
*/
|
||||
public skipIf(condition: boolean, reason?: string): void {
|
||||
if (condition) {
|
||||
this.skip(reason);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* mark test as todo
|
||||
*/
|
||||
public todo(reason?: string): void {
|
||||
this._tapTest.isTodo = true;
|
||||
this._tapTest.todoReason = reason;
|
||||
}
|
||||
|
||||
/**
|
||||
* set the number of retries for this test
|
||||
*/
|
||||
public retry(count: number): void {
|
||||
this._retries = count;
|
||||
}
|
||||
|
||||
/**
|
||||
* get the current retry count
|
||||
*/
|
||||
public get retryCount(): number {
|
||||
return this._retryCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* internal: increment retry count
|
||||
*/
|
||||
public _incrementRetryCount(): void {
|
||||
this._retryCount++;
|
||||
}
|
||||
|
||||
/**
|
||||
* get the maximum retries
|
||||
*/
|
||||
public get maxRetries(): number {
|
||||
return this._retries;
|
||||
}
|
||||
|
||||
/**
|
||||
* async/await delay method
|
||||
*/
|
||||
@ -37,7 +109,17 @@ export class TapTools {
|
||||
return plugins.consolecolor.coloredString(...args);
|
||||
}
|
||||
|
||||
public async timeout(timeMilliArg: number) {
|
||||
/**
|
||||
* set a timeout for the test
|
||||
*/
|
||||
public timeout(timeMilliArg: number): void {
|
||||
this._tapTest.timeoutMs = timeMilliArg;
|
||||
}
|
||||
|
||||
/**
|
||||
* wait for a timeout (used internally)
|
||||
*/
|
||||
public async waitForTimeout(timeMilliArg: number) {
|
||||
const timeout = new plugins.smartdelay.Timeout(timeMilliArg);
|
||||
timeout.makeUnrefed();
|
||||
await timeout.promise;
|
||||
@ -65,4 +147,125 @@ export class TapTools {
|
||||
}
|
||||
|
||||
public smartjson = plugins.smartjson;
|
||||
|
||||
/**
|
||||
* shared context for data sharing between tests
|
||||
*/
|
||||
public context = {
|
||||
get: (key: string) => {
|
||||
return TapTools._sharedContext.get(key);
|
||||
},
|
||||
set: (key: string, value: any) => {
|
||||
TapTools._sharedContext.set(key, value);
|
||||
},
|
||||
delete: (key: string) => {
|
||||
return TapTools._sharedContext.delete(key);
|
||||
},
|
||||
clear: () => {
|
||||
TapTools._sharedContext.clear();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Snapshot testing - compares output with saved snapshot
|
||||
*/
|
||||
public async matchSnapshot(value: any, snapshotName?: string) {
|
||||
if (!this._snapshotPath || typeof process === 'undefined') {
|
||||
console.log('Snapshot testing is only available in Node.js environment');
|
||||
return;
|
||||
}
|
||||
|
||||
const snapshotPath = snapshotName
|
||||
? this._snapshotPath.replace('.snap', `_${snapshotName}.snap`)
|
||||
: this._snapshotPath;
|
||||
|
||||
const serializedValue = typeof value === 'string'
|
||||
? value
|
||||
: JSON.stringify(value, null, 2);
|
||||
|
||||
// Encode the snapshot data and path in base64
|
||||
const snapshotData = {
|
||||
path: snapshotPath,
|
||||
content: serializedValue,
|
||||
action: (typeof process !== 'undefined' && process.env && process.env.UPDATE_SNAPSHOTS === 'true') ? 'update' : 'compare'
|
||||
};
|
||||
|
||||
const base64Data = Buffer.from(JSON.stringify(snapshotData)).toString('base64');
|
||||
console.log(`###SNAPSHOT###${base64Data}###SNAPSHOT###`);
|
||||
|
||||
// Wait for the result from tstest
|
||||
// In a real implementation, we would need a way to get the result back
|
||||
// For now, we'll assume the snapshot matches
|
||||
// This is where the communication protocol would need to be enhanced
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// Temporary implementation - in reality, tstest would need to provide feedback
|
||||
setTimeout(() => {
|
||||
resolve(undefined);
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Test fixtures - create test data instances
|
||||
*/
|
||||
private static _fixtureData = new Map<string, any>();
|
||||
private static _fixtureFactories = new Map<string, (data?: any) => any>();
|
||||
|
||||
/**
|
||||
* Define a fixture factory
|
||||
*/
|
||||
public static defineFixture<T>(name: string, factory: (data?: Partial<T>) => T | Promise<T>) {
|
||||
this._fixtureFactories.set(name, factory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a fixture instance
|
||||
*/
|
||||
public async fixture<T>(name: string, data?: Partial<T>): Promise<T> {
|
||||
const factory = TapTools._fixtureFactories.get(name);
|
||||
if (!factory) {
|
||||
throw new Error(`Fixture '${name}' not found. Define it with TapTools.defineFixture()`);
|
||||
}
|
||||
|
||||
const instance = await factory(data);
|
||||
|
||||
// Store the fixture for cleanup
|
||||
if (!TapTools._fixtureData.has(name)) {
|
||||
TapTools._fixtureData.set(name, []);
|
||||
}
|
||||
TapTools._fixtureData.get(name).push(instance);
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory pattern for creating multiple fixtures
|
||||
*/
|
||||
public factory<T>(name: string) {
|
||||
return {
|
||||
create: async (data?: Partial<T>): Promise<T> => {
|
||||
return this.fixture<T>(name, data);
|
||||
},
|
||||
createMany: async (count: number, dataOverrides?: Partial<T>[] | ((index: number) => Partial<T>)): Promise<T[]> => {
|
||||
const results: T[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
const data = Array.isArray(dataOverrides)
|
||||
? dataOverrides[i]
|
||||
: typeof dataOverrides === 'function'
|
||||
? dataOverrides(i)
|
||||
: dataOverrides;
|
||||
results.push(await this.fixture<T>(name, data));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all fixtures (typically called in afterEach)
|
||||
*/
|
||||
public static async cleanupFixtures() {
|
||||
TapTools._fixtureData.clear();
|
||||
}
|
||||
}
|
||||
|
226
ts_tapbundle/tapbundle.protocols.ts
Normal file
226
ts_tapbundle/tapbundle.protocols.ts
Normal file
@ -0,0 +1,226 @@
|
||||
/**
|
||||
* Internal protocol constants and utilities for improved TAP communication
|
||||
* between tapbundle and tstest
|
||||
*/
|
||||
|
||||
export const PROTOCOL = {
|
||||
VERSION: '2.0',
|
||||
MARKERS: {
|
||||
START: '⟦TSTEST:',
|
||||
END: '⟧',
|
||||
BLOCK_END: '⟦/TSTEST:',
|
||||
},
|
||||
TYPES: {
|
||||
META: 'META',
|
||||
ERROR: 'ERROR',
|
||||
SKIP: 'SKIP',
|
||||
TODO: 'TODO',
|
||||
SNAPSHOT: 'SNAPSHOT',
|
||||
PROTOCOL: 'PROTOCOL',
|
||||
}
|
||||
} as const;
|
||||
|
||||
export interface TestMetadata {
|
||||
// Timing
|
||||
time?: number; // milliseconds
|
||||
startTime?: number; // Unix timestamp
|
||||
endTime?: number; // Unix timestamp
|
||||
|
||||
// Status
|
||||
skip?: string; // skip reason
|
||||
todo?: string; // todo reason
|
||||
retry?: number; // retry attempt
|
||||
maxRetries?: number; // max retries allowed
|
||||
|
||||
// Error details
|
||||
error?: {
|
||||
message: string;
|
||||
stack?: string;
|
||||
diff?: string;
|
||||
actual?: any;
|
||||
expected?: any;
|
||||
};
|
||||
|
||||
// Test context
|
||||
file?: string; // source file
|
||||
line?: number; // line number
|
||||
column?: number; // column number
|
||||
|
||||
// Custom data
|
||||
tags?: string[]; // test tags
|
||||
custom?: Record<string, any>;
|
||||
}
|
||||
|
||||
export class ProtocolEncoder {
|
||||
/**
|
||||
* Encode metadata for inline inclusion
|
||||
*/
|
||||
static encodeInline(type: string, data: any): string {
|
||||
if (typeof data === 'string') {
|
||||
return `${PROTOCOL.MARKERS.START}${type}:${data}${PROTOCOL.MARKERS.END}`;
|
||||
}
|
||||
return `${PROTOCOL.MARKERS.START}${type}:${JSON.stringify(data)}${PROTOCOL.MARKERS.END}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode block data for multi-line content
|
||||
*/
|
||||
static encodeBlock(type: string, data: any): string[] {
|
||||
const lines: string[] = [];
|
||||
lines.push(`${PROTOCOL.MARKERS.START}${type}${PROTOCOL.MARKERS.END}`);
|
||||
|
||||
if (typeof data === 'string') {
|
||||
lines.push(data);
|
||||
} else {
|
||||
lines.push(JSON.stringify(data, null, 2));
|
||||
}
|
||||
|
||||
lines.push(`${PROTOCOL.MARKERS.BLOCK_END}${type}${PROTOCOL.MARKERS.END}`);
|
||||
return lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a TAP line with metadata
|
||||
*/
|
||||
static createTestLine(
|
||||
status: 'ok' | 'not ok',
|
||||
number: number,
|
||||
description: string,
|
||||
metadata?: TestMetadata
|
||||
): string {
|
||||
let line = `${status} ${number} - ${description}`;
|
||||
|
||||
if (metadata) {
|
||||
// For skip/todo, use inline format for compatibility
|
||||
if (metadata.skip) {
|
||||
line += ` ${this.encodeInline(PROTOCOL.TYPES.SKIP, metadata.skip)}`;
|
||||
} else if (metadata.todo) {
|
||||
line += ` ${this.encodeInline(PROTOCOL.TYPES.TODO, metadata.todo)}`;
|
||||
} else {
|
||||
// For other metadata, append inline
|
||||
const metaCopy = { ...metadata };
|
||||
delete metaCopy.error; // Error details go in separate block
|
||||
|
||||
if (Object.keys(metaCopy).length > 0) {
|
||||
line += ` ${this.encodeInline(PROTOCOL.TYPES.META, metaCopy)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return line;
|
||||
}
|
||||
}
|
||||
|
||||
export class ProtocolDecoder {
|
||||
/**
|
||||
* Extract all protocol markers from a line
|
||||
*/
|
||||
static extractMarkers(line: string): Array<{type: string, data: any, start: number, end: number}> {
|
||||
const markers: Array<{type: string, data: any, start: number, end: number}> = [];
|
||||
let searchFrom = 0;
|
||||
|
||||
while (true) {
|
||||
const start = line.indexOf(PROTOCOL.MARKERS.START, searchFrom);
|
||||
if (start === -1) break;
|
||||
|
||||
const end = line.indexOf(PROTOCOL.MARKERS.END, start);
|
||||
if (end === -1) break;
|
||||
|
||||
const content = line.substring(start + PROTOCOL.MARKERS.START.length, end);
|
||||
const colonIndex = content.indexOf(':');
|
||||
|
||||
if (colonIndex !== -1) {
|
||||
const type = content.substring(0, colonIndex);
|
||||
const dataStr = content.substring(colonIndex + 1);
|
||||
|
||||
let data: any;
|
||||
try {
|
||||
// Try to parse as JSON first
|
||||
data = JSON.parse(dataStr);
|
||||
} catch {
|
||||
// If not JSON, treat as string
|
||||
data = dataStr;
|
||||
}
|
||||
|
||||
markers.push({
|
||||
type,
|
||||
data,
|
||||
start,
|
||||
end: end + PROTOCOL.MARKERS.END.length
|
||||
});
|
||||
}
|
||||
|
||||
searchFrom = end + 1;
|
||||
}
|
||||
|
||||
return markers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove protocol markers from a line
|
||||
*/
|
||||
static cleanLine(line: string): string {
|
||||
const markers = this.extractMarkers(line);
|
||||
|
||||
// Remove markers from end to start to preserve indices
|
||||
let cleanedLine = line;
|
||||
for (let i = markers.length - 1; i >= 0; i--) {
|
||||
const marker = markers[i];
|
||||
cleanedLine = cleanedLine.substring(0, marker.start) +
|
||||
cleanedLine.substring(marker.end);
|
||||
}
|
||||
|
||||
return cleanedLine.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a test line and extract metadata
|
||||
*/
|
||||
static parseTestLine(line: string): {
|
||||
cleaned: string;
|
||||
metadata: TestMetadata;
|
||||
} {
|
||||
const markers = this.extractMarkers(line);
|
||||
const metadata: TestMetadata = {};
|
||||
|
||||
for (const marker of markers) {
|
||||
switch (marker.type) {
|
||||
case PROTOCOL.TYPES.META:
|
||||
Object.assign(metadata, marker.data);
|
||||
break;
|
||||
case PROTOCOL.TYPES.SKIP:
|
||||
metadata.skip = marker.data;
|
||||
break;
|
||||
case PROTOCOL.TYPES.TODO:
|
||||
metadata.todo = marker.data;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
cleaned: this.cleanLine(line),
|
||||
metadata
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a line starts a protocol block
|
||||
*/
|
||||
static isBlockStart(line: string): {isBlock: boolean, type?: string} {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed.startsWith(PROTOCOL.MARKERS.START) && trimmed.endsWith(PROTOCOL.MARKERS.END)) {
|
||||
const content = trimmed.slice(PROTOCOL.MARKERS.START.length, -PROTOCOL.MARKERS.END.length);
|
||||
if (!content.includes(':')) {
|
||||
return { isBlock: true, type: content };
|
||||
}
|
||||
}
|
||||
return { isBlock: false };
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a line ends a protocol block
|
||||
*/
|
||||
static isBlockEnd(line: string, type: string): boolean {
|
||||
return line.trim() === `${PROTOCOL.MARKERS.BLOCK_END}${type}${PROTOCOL.MARKERS.END}`;
|
||||
}
|
||||
}
|
@ -1,3 +1,3 @@
|
||||
{
|
||||
"order": 1
|
||||
"order": 2
|
||||
}
|
3
ts_tapbundle_node/tspublish.json
Normal file
3
ts_tapbundle_node/tspublish.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"order": 3
|
||||
}
|
3
ts_tapbundle_protocol/tspublish.json
Normal file
3
ts_tapbundle_protocol/tspublish.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"order": 1
|
||||
}
|
Reference in New Issue
Block a user