Compare commits
52 Commits
Author | SHA1 | Date | |
---|---|---|---|
a3a4ded41e | |||
03d478d6ff | |||
77e53bd68a | |||
946e467c26 | |||
f452a58fff | |||
2b01d949f2 | |||
1c5cf46ba9 | |||
b28e2eace3 | |||
cc388f1408 | |||
bac2f852c5 | |||
d9e0f1f758 | |||
42cd08eb1c | |||
553d5f0df7 | |||
6cc883dede | |||
fa9abbc4db | |||
56f0f0be16 | |||
dc0f859fad | |||
78ffad2f7d | |||
3fc4cee2b1 | |||
a57edeef64 | |||
1f73751a8c | |||
90741ed917 | |||
962fa2cd4d | |||
c085a20a4f | |||
1f355a10a1 | |||
a73ce99564 | |||
64f825091d | |||
5ddc2d2de0 | |||
85fec03878 | |||
61c3226156 | |||
f0bf778810 | |||
a8e9f67810 | |||
4cce132472 | |||
dc250804f5 | |||
9669445646 | |||
928d9d0616 | |||
3655b2f734 | |||
6712ff6b07 | |||
ef5efc0a93 | |||
f305547116 | |||
033a0a806c | |||
7f87c24ad8 | |||
ac08bdffe5 | |||
eb64cb4f71 | |||
3b56c6ce9f | |||
722d777f80 | |||
f1a0455662 | |||
3c62129e02 | |||
ac5e036967 | |||
6ccd0281b9 | |||
d0f85b026f | |||
4376cafabb |
.gitlab-ci.ymlchangelog.mdpackage.jsonpnpm-lock.yamlreadme.hints.mdreadme.mdreadme.plan.md
test
debug.js
tapbundle
test.browser.nonci.tstest.debug.tstest.describe.tstest.fixtures.tstest.fluent-syntax.tstest.node.tstest.snapshot.tstest.tags-context.tstest.tapwrap.tstest.toolsarg.tstest.ts
test.tststest
ts
00_commitinfo_data.tsindex.tstspublish.jsontstest.classes.tap.combinator.tststest.classes.tap.parser.tststest.classes.testdirectory.tststest.classes.tstest.tststest.logging.tststest.logprefixes.tststest.plugins.ts
ts_tapbundle
00_commitinfo_data.tsindex.tstapbundle.classes.pretask.tstapbundle.classes.tap.tstapbundle.classes.taptest.tstapbundle.classes.taptools.tstapbundle.classes.tapwrap.tstapbundle.plugins.tstapbundle.tapcreator.tstspublish.jsonwebhelpers.ts
ts_tapbundle_node
tsconfig.json
128
.gitlab-ci.yml
128
.gitlab-ci.yml
@ -1,128 +0,0 @@
|
|||||||
# gitzone ci_default
|
|
||||||
image: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
|
||||||
|
|
||||||
cache:
|
|
||||||
paths:
|
|
||||||
- .npmci_cache/
|
|
||||||
key: '$CI_BUILD_STAGE'
|
|
||||||
|
|
||||||
stages:
|
|
||||||
- security
|
|
||||||
- test
|
|
||||||
- release
|
|
||||||
- metadata
|
|
||||||
|
|
||||||
before_script:
|
|
||||||
- pnpm install -g pnpm
|
|
||||||
- pnpm install -g @shipzone/npmci
|
|
||||||
- npmci npm prepare
|
|
||||||
|
|
||||||
# ====================
|
|
||||||
# security stage
|
|
||||||
# ====================
|
|
||||||
# ====================
|
|
||||||
# security stage
|
|
||||||
# ====================
|
|
||||||
auditProductionDependencies:
|
|
||||||
image: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
|
||||||
stage: security
|
|
||||||
script:
|
|
||||||
- npmci command npm config set registry https://registry.npmjs.org
|
|
||||||
- npmci command pnpm audit --audit-level=high --prod
|
|
||||||
tags:
|
|
||||||
- lossless
|
|
||||||
- docker
|
|
||||||
allow_failure: true
|
|
||||||
|
|
||||||
auditDevDependencies:
|
|
||||||
image: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
|
||||||
stage: security
|
|
||||||
script:
|
|
||||||
- npmci command npm config set registry https://registry.npmjs.org
|
|
||||||
- npmci command pnpm audit --audit-level=high --dev
|
|
||||||
tags:
|
|
||||||
- lossless
|
|
||||||
- docker
|
|
||||||
allow_failure: true
|
|
||||||
|
|
||||||
# ====================
|
|
||||||
# test stage
|
|
||||||
# ====================
|
|
||||||
|
|
||||||
testStable:
|
|
||||||
stage: test
|
|
||||||
script:
|
|
||||||
- npmci node install stable
|
|
||||||
- npmci npm install
|
|
||||||
- npmci npm test
|
|
||||||
coverage: /\d+.?\d+?\%\s*coverage/
|
|
||||||
tags:
|
|
||||||
- docker
|
|
||||||
|
|
||||||
testBuild:
|
|
||||||
stage: test
|
|
||||||
script:
|
|
||||||
- npmci node install stable
|
|
||||||
- npmci npm install
|
|
||||||
- npmci command npm run build
|
|
||||||
coverage: /\d+.?\d+?\%\s*coverage/
|
|
||||||
tags:
|
|
||||||
- docker
|
|
||||||
|
|
||||||
release:
|
|
||||||
stage: release
|
|
||||||
script:
|
|
||||||
- npmci node install stable
|
|
||||||
- npmci npm publish
|
|
||||||
only:
|
|
||||||
- tags
|
|
||||||
tags:
|
|
||||||
- lossless
|
|
||||||
- docker
|
|
||||||
- notpriv
|
|
||||||
|
|
||||||
# ====================
|
|
||||||
# metadata stage
|
|
||||||
# ====================
|
|
||||||
codequality:
|
|
||||||
stage: metadata
|
|
||||||
allow_failure: true
|
|
||||||
only:
|
|
||||||
- tags
|
|
||||||
script:
|
|
||||||
- npmci command npm install -g typescript
|
|
||||||
- npmci npm prepare
|
|
||||||
- npmci npm install
|
|
||||||
tags:
|
|
||||||
- lossless
|
|
||||||
- docker
|
|
||||||
- priv
|
|
||||||
|
|
||||||
trigger:
|
|
||||||
stage: metadata
|
|
||||||
script:
|
|
||||||
- npmci trigger
|
|
||||||
only:
|
|
||||||
- tags
|
|
||||||
tags:
|
|
||||||
- lossless
|
|
||||||
- docker
|
|
||||||
- notpriv
|
|
||||||
|
|
||||||
pages:
|
|
||||||
stage: metadata
|
|
||||||
script:
|
|
||||||
- npmci node install stable
|
|
||||||
- npmci npm install
|
|
||||||
- npmci command npm run buildDocs
|
|
||||||
tags:
|
|
||||||
- lossless
|
|
||||||
- docker
|
|
||||||
- notpriv
|
|
||||||
only:
|
|
||||||
- tags
|
|
||||||
artifacts:
|
|
||||||
expire_in: 1 week
|
|
||||||
paths:
|
|
||||||
- public
|
|
||||||
allow_failure: true
|
|
235
changelog.md
Normal file
235
changelog.md
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
- Replaced 'main' and 'typings' in package.json with explicit exports for improved module resolution.
|
||||||
|
- Added .claude/settings.local.json to configure permissions for bash commands and web fetches.
|
||||||
|
- Updated readme.plan.md with a comprehensive roadmap covering enhanced error reporting, rich test metadata, nested test suites, and advanced test features.
|
||||||
|
|
||||||
|
## 2025-05-15 - 1.5.0 - feat(cli)
|
||||||
|
Improve test runner configuration: update test scripts, reorganize test directories, update dependencies and add local settings for command permissions.
|
||||||
|
|
||||||
|
- Updated package.json scripts to use pnpm and separate commands for tapbundle and tstest.
|
||||||
|
- Reorganized tests into dedicated directories (test/tapbundle and test/tstest) and removed deprecated test files.
|
||||||
|
- Refactored import paths and bumped dependency versions in tapbundle, tstest, and associated node utilities.
|
||||||
|
- Added .claude/settings.local.json to configure local permissions for bash and web fetch commands.
|
||||||
|
- Introduced ts/tspublish.json to define publish order.
|
||||||
|
|
||||||
|
## 2025-05-15 - 1.4.0 - feat(logging)
|
||||||
|
Display failed test console logs in default mode
|
||||||
|
|
||||||
|
- Introduce log buffering in TsTestLogger to capture console output for failed tests
|
||||||
|
- Enhance TapParser to collect and display error details when tests fail
|
||||||
|
- Update README and project plan to document log improvements for debugging
|
||||||
|
|
||||||
|
## 2025-05-15 - 1.3.1 - fix(settings)
|
||||||
|
Add local permissions configuration and remove obsolete test output log
|
||||||
|
|
||||||
|
- Added .claude/settings.local.json to configure allowed permissions for web fetch and bash commands
|
||||||
|
- Removed test-output.log to eliminate accidental commit of test artifacts
|
||||||
|
|
||||||
|
## 2025-05-15 - 1.3.0 - feat(logger)
|
||||||
|
Improve logging output and add --logfile support for persistent logs
|
||||||
|
|
||||||
|
- Add new .claude/settings.local.json with logging permissions configuration
|
||||||
|
- Remove obsolete readme.plan.md
|
||||||
|
- Introduce test/test.console.ts to capture and display console outputs during tests
|
||||||
|
- Update CLI in ts/index.ts to replace '--log-file' with '--logfile' flag
|
||||||
|
- Enhance TsTestLogger to support file logging, clean ANSI sequences, and improved JSON output
|
||||||
|
- Forward TAP protocol logs to testConsoleOutput in TapParser for better console distinction
|
||||||
|
|
||||||
|
## 2025-05-15 - 1.2.0 - feat(logging)
|
||||||
|
Improve logging output, CLI option parsing, and test report formatting.
|
||||||
|
|
||||||
|
- Added a centralized TsTestLogger with support for multiple verbosity levels, JSON output, and file logging (TODO).
|
||||||
|
- Integrated new logger into CLI parsing, TapParser, TapCombinator, and TsTest classes to ensure consistent and structured output.
|
||||||
|
- Introduced new CLI options (--quiet, --verbose, --no-color, --json, --log-file) for enhanced user control.
|
||||||
|
- Enhanced visual design with progress indicators, detailed error aggregation, and performance summaries.
|
||||||
|
- Updated documentation and logging code to align with improved CI/CD behavior, including skipping non-CI tests.
|
||||||
|
|
||||||
|
## 2025-05-15 - 1.1.0 - feat(cli)
|
||||||
|
Enhance test discovery with support for single file and glob pattern execution using improved CLI argument detection
|
||||||
|
|
||||||
|
- Detect execution mode (file, glob, directory) based on CLI input in ts/index.ts
|
||||||
|
- Refactor TestDirectory to load test files using SmartFile for single file and glob patterns
|
||||||
|
- Update TsTest to pass execution mode and adjust test discovery accordingly
|
||||||
|
- Bump dependency versions for typedserver, tsbundle, tapbundle, and others
|
||||||
|
- Add .claude/settings.local.json for updated permissions configuration
|
||||||
|
|
||||||
|
## 2025-01-23 - 1.0.96 - fix(TsTest)
|
||||||
|
Fixed improper type-check for promise-like testModule defaults
|
||||||
|
|
||||||
|
- Corrected the type-check for promise-like default exports in test modules
|
||||||
|
- Removed unnecessary setTimeout used for async execution
|
||||||
|
|
||||||
|
## 2025-01-23 - 1.0.95 - fix(core)
|
||||||
|
Fix delay handling in Chrome test execution
|
||||||
|
|
||||||
|
- Replaced smartdelay.delayFor with native Promise-based delay mechanism in runInChrome method.
|
||||||
|
|
||||||
|
## 2025-01-23 - 1.0.94 - fix(TsTest)
|
||||||
|
Fix test module execution by ensuring promise resolution delay
|
||||||
|
|
||||||
|
- Added a delay to ensure promise resolution when dynamically importing test modules in the runInChrome method.
|
||||||
|
|
||||||
|
## 2025-01-23 - 1.0.93 - fix(tstest)
|
||||||
|
Handle globalThis.tapPromise in browser runtime evaluation
|
||||||
|
|
||||||
|
- Added support for using globalThis.tapPromise in the browser evaluation logic.
|
||||||
|
- Added log messages to indicate the usage of globalThis.tapPromise.
|
||||||
|
|
||||||
|
## 2025-01-23 - 1.0.92 - fix(core)
|
||||||
|
Improve error logging for test modules without default promise
|
||||||
|
|
||||||
|
- Added logging to display the exported test module content when it does not export a default promise.
|
||||||
|
|
||||||
|
## 2025-01-23 - 1.0.91 - fix(core)
|
||||||
|
Refactored tstest class to enhance promise handling for test modules.
|
||||||
|
|
||||||
|
- Removed .gitlab-ci.yml configuration file.
|
||||||
|
- Updated package.json dependency versions.
|
||||||
|
- Added a condition to handle promiselike objects in tests.
|
||||||
|
|
||||||
|
## 2024-04-18 - 1.0.89 to 1.0.90 - Enhancements and Bug Fixes
|
||||||
|
Multiple updates and fixes have been made.
|
||||||
|
|
||||||
|
- Updated core components to enhance stability and performance.
|
||||||
|
|
||||||
|
## 2024-03-07 - 1.0.86 to 1.0.88 - Core Updates
|
||||||
|
Continued improvements and updates in the core module.
|
||||||
|
|
||||||
|
- Applied critical fixes to enhance core stability.
|
||||||
|
|
||||||
|
## 2024-01-19 - 1.0.85 to 1.0.89 - Bug Fixes
|
||||||
|
Series of core updates have been implemented.
|
||||||
|
|
||||||
|
- Addressed known bugs and improved overall system functionality.
|
||||||
|
|
||||||
|
## 2023-11-09 - 1.0.81 to 1.0.84 - Maintenance Updates
|
||||||
|
Maintenance updates focusing on core reliability.
|
||||||
|
|
||||||
|
- Improved core module through systematic updates.
|
||||||
|
- Strengthened system robustness.
|
||||||
|
|
||||||
|
## 2023-08-26 - 1.0.77 to 1.0.80 - Critical Fixes
|
||||||
|
Critical fixes implemented in core functionality.
|
||||||
|
|
||||||
|
- Enhanced core processing to fix existing issues.
|
||||||
|
|
||||||
|
## 2023-07-13 - 1.0.75 to 1.0.76 - Stability Improvements
|
||||||
|
Stability enhancements and minor improvements.
|
||||||
|
|
||||||
|
- Focused on ensuring a stable operational core.
|
||||||
|
|
||||||
|
## 2022-11-08 - 1.0.73 to 1.0.74 - Routine Fixes
|
||||||
|
Routine core fixes to address reported issues.
|
||||||
|
|
||||||
|
- Addressed minor issues in the core module.
|
||||||
|
|
||||||
|
## 2022-08-03 - 1.0.71 to 1.0.72 - Core Enhancements
|
||||||
|
Enhancements applied to core systems.
|
||||||
|
|
||||||
|
- Tweaked core components for enhanced reliability.
|
||||||
|
|
||||||
|
## 2022-05-04 - 1.0.69 to 1.0.70 - System Reliability Fixes
|
||||||
|
Fixes targeting the reliability of the core systems.
|
||||||
|
|
||||||
|
- Improved system reliability through targeted core updates.
|
||||||
|
|
||||||
|
## 2022-03-17 - 1.0.65 to 1.0.68 - Major Core Updates
|
||||||
|
Major updates and bug fixes delivered for core components.
|
||||||
|
|
||||||
|
- Enhanced central operations through key updates.
|
||||||
|
|
||||||
|
## 2022-02-15 - 1.0.60 to 1.0.64 - Core Stability Improvements
|
||||||
|
Focused updates on core stability and performance.
|
||||||
|
|
||||||
|
- Reinforced stability through systematic core changes.
|
||||||
|
|
||||||
|
## 2021-11-07 - 1.0.54 to 1.0.59 - Core Fixes and Improvements
|
||||||
|
Multiple core updates aimed at fixing and improving the system.
|
||||||
|
|
||||||
|
- Addressed outstanding bugs and improved performance in the core.
|
||||||
|
|
||||||
|
## 2021-08-20 - 1.0.50 to 1.0.53 - Core Functionality Updates
|
||||||
|
Continued updates to improve core functionality and user experience.
|
||||||
|
|
||||||
|
- Implemented essential core fixes to enhance user experience.
|
||||||
|
|
||||||
|
## 2020-10-01 - 1.0.44 to 1.0.49 - Core System Enhancements
|
||||||
|
Critical enhancements to core systems.
|
||||||
|
|
||||||
|
- Improved core operations and tackled existing issues.
|
||||||
|
|
||||||
|
## 2020-09-29 - 1.0.40 to 1.0.43 - Essential Fixes
|
||||||
|
Series of essential fixes for the core system.
|
||||||
|
|
||||||
|
- Rectified known issues and bolstered core functionalities.
|
||||||
|
|
||||||
|
## 2020-07-10 - 1.0.35 to 1.0.39 - Core Function Fixes
|
||||||
|
Focused improvements and fixes for critical components.
|
||||||
|
|
||||||
|
- Addressed critical core functions to boost system performance.
|
||||||
|
|
||||||
|
## 2020-06-01 - 1.0.31 to 1.0.34 - Core Updates
|
||||||
|
Updates to maintain core functionality efficacy.
|
||||||
|
|
||||||
|
- Fixed inefficiencies and updated essential components.
|
||||||
|
|
||||||
|
## 2019-10-02 - 1.0.26 to 1.0.29 - Core Maintenance
|
||||||
|
Regular maintenance and updates for core reliability.
|
||||||
|
|
||||||
|
- Addressed multiple core issues and enhanced system stability.
|
||||||
|
|
||||||
|
## 2019-05-28 - 1.0.20 to 1.0.25 - Core Improvements
|
||||||
|
General improvements targeting core functionalities.
|
||||||
|
|
||||||
|
- Made systematic improvements to core processes.
|
||||||
|
|
||||||
|
## 2019-04-08 - 1.0.16 to 1.0.19 - Bug Squashing
|
||||||
|
Resolved numerous issues within core operations.
|
||||||
|
|
||||||
|
- Fixed and optimized core functionalities for better performance.
|
||||||
|
|
||||||
|
## 2018-12-06 - 1.0.15 - Dependency Updates
|
||||||
|
Updates aimed at improving dependency management.
|
||||||
|
|
||||||
|
- Ensured dependencies are up-to-date for optimal performance.
|
||||||
|
|
||||||
|
## 2018-08-14 - 1.0.14 - Test Improvement
|
||||||
|
Major improvements in testing mechanisms and logging.
|
||||||
|
|
||||||
|
- Improved test results handling for accuracy and reliability.
|
||||||
|
- Enhanced logging features for increased clarity.
|
||||||
|
|
||||||
|
## 2018-08-04 - 1.0.1 to 1.0.13 - Initial Implementation and Fixes
|
||||||
|
Initial release and critical updates focusing on core stability and functionality.
|
||||||
|
|
||||||
|
- Implemented core components and established initial system structure.
|
||||||
|
- Addressed key bugs and enhanced initial functionality.
|
60
package.json
60
package.json
@ -1,10 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "@git.zone/tstest",
|
"name": "@git.zone/tstest",
|
||||||
"version": "1.0.80",
|
"version": "1.9.0",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "a test utility to run tests that match test/**/*.ts",
|
"description": "a test utility to run tests that match test/**/*.ts",
|
||||||
"main": "dist_ts/index.js",
|
"exports": {
|
||||||
"typings": "dist_ts/index.d.ts",
|
".": "./dist_ts/index.js",
|
||||||
|
"./tapbundle": "./dist_ts_tapbundle/index.js",
|
||||||
|
"./tapbundle_node": "./dist_ts_tapbundle_node/index.js"
|
||||||
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"author": "Lossless GmbH",
|
"author": "Lossless GmbH",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@ -12,30 +15,42 @@
|
|||||||
"tstest": "./cli.js"
|
"tstest": "./cli.js"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "(npm run cleanUp && npm run prepareTest && npm run tstest)",
|
"test": "pnpm run build && pnpm run test:tapbundle:verbose && pnpm run test:tstest:verbose",
|
||||||
"prepareTest": "git clone https://gitlab.com/sandboxzone/sandbox-npmts.git .nogit/sandbox-npmts && cd .nogit/sandbox-npmts && npm install",
|
"test:tapbundle": "tsx ./cli.child.ts \"test/tapbundle/**/*.ts\"",
|
||||||
"tstest": "cd .nogit/sandbox-npmts && node ../../cli.ts.js test/ --web",
|
"test:tapbundle:verbose": "tsx ./cli.child.ts \"test/tapbundle/**/*.ts\" --verbose",
|
||||||
"cleanUp": "rm -rf .nogit/sandbox-npmts",
|
"test:tstest": "tsx ./cli.child.ts \"test/tstest/**/*.ts\"",
|
||||||
"build": "(tsbuild --web --allowimplicitany --skiplibcheck)",
|
"test:tstest:verbose": "tsx ./cli.child.ts \"test/tstest/**/*.ts\" --verbose",
|
||||||
|
"build": "(tsbuild tsfolders)",
|
||||||
"buildDocs": "tsdoc"
|
"buildDocs": "tsdoc"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@git.zone/tsbuild": "^2.1.69",
|
"@git.zone/tsbuild": "^2.5.1",
|
||||||
"@types/node": "^20.5.6"
|
"@types/node": "^22.15.18"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apiglobal/typedserver": "^2.0.65",
|
"@api.global/typedserver": "^3.0.74",
|
||||||
"@git.zone/tsbundle": "^2.0.8",
|
"@git.zone/tsbundle": "^2.2.5",
|
||||||
"@git.zone/tsrun": "^1.2.46",
|
"@git.zone/tsrun": "^1.3.3",
|
||||||
"@push.rocks/consolecolor": "^2.0.1",
|
"@push.rocks/consolecolor": "^2.0.2",
|
||||||
"@push.rocks/smartbrowser": "^2.0.5",
|
"@push.rocks/qenv": "^6.1.0",
|
||||||
|
"@push.rocks/smartbrowser": "^2.0.8",
|
||||||
|
"@push.rocks/smartcrypto": "^2.0.4",
|
||||||
"@push.rocks/smartdelay": "^3.0.5",
|
"@push.rocks/smartdelay": "^3.0.5",
|
||||||
"@push.rocks/smartfile": "^10.0.30",
|
"@push.rocks/smartenv": "^5.0.12",
|
||||||
"@push.rocks/smartlog": "^3.0.3",
|
"@push.rocks/smartexpect": "^2.4.2",
|
||||||
"@push.rocks/smartpromise": "^4.0.3",
|
"@push.rocks/smartfile": "^11.2.0",
|
||||||
"@push.rocks/smartshell": "^3.0.3",
|
"@push.rocks/smartjson": "^5.0.20",
|
||||||
"@push.rocks/tapbundle": "^5.0.15",
|
"@push.rocks/smartlog": "^3.1.1",
|
||||||
"figures": "^5.0.0"
|
"@push.rocks/smartmongo": "^2.0.12",
|
||||||
|
"@push.rocks/smartpath": "^5.0.18",
|
||||||
|
"@push.rocks/smartpromise": "^4.2.3",
|
||||||
|
"@push.rocks/smartrequest": "^2.1.0",
|
||||||
|
"@push.rocks/smarts3": "^2.2.5",
|
||||||
|
"@push.rocks/smartshell": "^3.2.3",
|
||||||
|
"@push.rocks/smarttime": "^4.1.1",
|
||||||
|
"@types/ws": "^8.18.1",
|
||||||
|
"figures": "^6.1.0",
|
||||||
|
"ws": "^8.18.2"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"ts/**/*",
|
"ts/**/*",
|
||||||
@ -51,5 +66,6 @@
|
|||||||
],
|
],
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"last 1 chrome versions"
|
"last 1 chrome versions"
|
||||||
]
|
],
|
||||||
|
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
|
||||||
}
|
}
|
||||||
|
11601
pnpm-lock.yaml
generated
11601
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
62
readme.hints.md
Normal file
62
readme.hints.md
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
# Architecture Overview
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
This project integrates tstest with tapbundle through a modular architecture:
|
||||||
|
|
||||||
|
1. **tstest** (`/ts/`) - The test runner that discovers and executes test files
|
||||||
|
2. **tapbundle** (`/ts_tapbundle/`) - The TAP testing framework for writing tests
|
||||||
|
3. **tapbundle_node** (`/ts_tapbundle_node/`) - Node.js-specific testing utilities
|
||||||
|
|
||||||
|
## How Components Work Together
|
||||||
|
|
||||||
|
### Test Execution Flow
|
||||||
|
|
||||||
|
1. **CLI Entry Point** (`cli.js` ’ `cli.ts.js` ’ `cli.child.ts`)
|
||||||
|
- The CLI uses tsx to run TypeScript files directly
|
||||||
|
- Accepts glob patterns to find test files
|
||||||
|
- Supports options like `--verbose`, `--quiet`, `--web`
|
||||||
|
|
||||||
|
2. **Test Discovery**
|
||||||
|
- tstest scans for test files matching the provided pattern
|
||||||
|
- Defaults to `test/**/*.ts` when no pattern is specified
|
||||||
|
- Supports both file and directory modes
|
||||||
|
|
||||||
|
3. **Test Runner**
|
||||||
|
- Each test file imports `tap` and `expect` from tapbundle
|
||||||
|
- Tests are written using `tap.test()` with async functions
|
||||||
|
- Browser tests are compiled with esbuild and run in Chromium via Puppeteer
|
||||||
|
|
||||||
|
### Key Integration Points
|
||||||
|
|
||||||
|
1. **Import Structure**
|
||||||
|
- Test files import from local tapbundle: `import { tap, expect } from '../../ts_tapbundle/index.js'`
|
||||||
|
- Node-specific tests also import from tapbundle_node: `import { tapNodeTools } from '../../ts_tapbundle_node/index.js'`
|
||||||
|
|
||||||
|
2. **WebHelpers**
|
||||||
|
- Browser tests can use webhelpers for DOM manipulation
|
||||||
|
- `webhelpers.html` - Template literal for creating HTML strings
|
||||||
|
- `webhelpers.fixture` - Creates DOM elements from HTML strings
|
||||||
|
- Automatically detects browser environment and only enables in browser context
|
||||||
|
|
||||||
|
3. **Build System**
|
||||||
|
- Uses `tsbuild tsfolders` to compile TypeScript
|
||||||
|
- Maintains separate output directories: `/dist_ts/`, `/dist_ts_tapbundle/`, `/dist_ts_tapbundle_node/`
|
||||||
|
- Compilation order is resolved automatically based on dependencies
|
||||||
|
|
||||||
|
### Test Scripts
|
||||||
|
|
||||||
|
The package.json defines several test scripts:
|
||||||
|
- `test` - Builds and runs all tests (tapbundle and tstest)
|
||||||
|
- `test:tapbundle` - Runs tapbundle framework tests
|
||||||
|
- `test:tstest` - Runs tstest's own tests
|
||||||
|
- Both support `:verbose` variants for detailed output
|
||||||
|
|
||||||
|
### Environment Detection
|
||||||
|
|
||||||
|
The framework automatically detects the runtime environment:
|
||||||
|
- Node.js tests run directly via tsx
|
||||||
|
- Browser tests are compiled and served via a local server
|
||||||
|
- WebHelpers are only enabled in browser environment
|
||||||
|
|
||||||
|
This architecture allows for seamless testing across both Node.js and browser environments while maintaining a clean separation of concerns.
|
357
readme.md
357
readme.md
@ -1,59 +1,352 @@
|
|||||||
# @gitzone/tstest
|
# @gitzone/tstest
|
||||||
a test utility to run tests that match test/**/*.ts
|
🧪 **A powerful, modern test runner for TypeScript** - making your test runs beautiful and informative!
|
||||||
|
|
||||||
## Availabililty and Links
|
## Availabililty and Links
|
||||||
* [npmjs.org (npm package)](https://www.npmjs.com/package/@gitzone/tstest)
|
* [npmjs.org (npm package)](https://www.npmjs.com/package/@gitzone/tstest)
|
||||||
* [gitlab.com (source)](https://gitlab.com/gitzone/tstest)
|
* [code.foss.global (source)](https://code.foss.global/gitzone/tstest)
|
||||||
* [github.com (source mirror)](https://github.com/gitzone/tstest)
|
|
||||||
* [docs (typedoc)](https://gitzone.gitlab.io/tstest/)
|
|
||||||
|
|
||||||
## Status for master
|
## Why tstest?
|
||||||
|
|
||||||
Status Category | Status Badge
|
**tstest** is a TypeScript test runner that makes testing delightful. It's designed for modern development workflows with beautiful output, flexible test execution, and powerful features that make debugging a breeze.
|
||||||
-- | --
|
|
||||||
GitLab Pipelines | [](https://lossless.cloud)
|
### ✨ Key Features
|
||||||
GitLab Pipline Test Coverage | [](https://lossless.cloud)
|
|
||||||
npm | [](https://lossless.cloud)
|
- 🎯 **Smart Test Execution** - Run all tests, single files, or use glob patterns
|
||||||
Snyk | [](https://lossless.cloud)
|
- 🎨 **Beautiful Output** - Color-coded results with emojis and clean formatting
|
||||||
TypeScript Support | [](https://lossless.cloud)
|
- 📊 **Multiple Output Modes** - Choose from normal, quiet, verbose, or JSON output
|
||||||
node Support | [](https://nodejs.org/dist/latest-v10.x/docs/api/)
|
- 🔍 **Automatic Discovery** - Finds all your test files automatically
|
||||||
Code Style | [](https://lossless.cloud)
|
- 🌐 **Cross-Environment** - Supports Node.js and browser testing
|
||||||
PackagePhobia (total standalone install weight) | [](https://lossless.cloud)
|
- 📝 **Detailed Logging** - Optional file logging for debugging
|
||||||
PackagePhobia (package size on registry) | [](https://lossless.cloud)
|
- ⚡ **Performance Metrics** - See which tests are slow
|
||||||
BundlePhobia (total size when bundled) | [](https://lossless.cloud)
|
- 🤖 **CI/CD Ready** - JSON output mode for automation
|
||||||
Platform support | [](https://lossless.cloud) [](https://lossless.cloud)
|
- 🏷️ **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
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install --save-dev @gitzone/tstest
|
||||||
|
# or with pnpm
|
||||||
|
pnpm add -D @gitzone/tstest
|
||||||
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
## cli usage
|
### Basic Test Execution
|
||||||
|
|
||||||
lets assume we have a directory called test/ where all our tests arae defined. Simply type
|
```bash
|
||||||
|
# Run all tests in a directory
|
||||||
```
|
|
||||||
tstest test/
|
tstest test/
|
||||||
|
|
||||||
|
# Run a specific test file
|
||||||
|
tstest test/test.mycomponent.ts
|
||||||
|
|
||||||
|
# Use glob patterns
|
||||||
|
tstest "test/**/*.spec.ts"
|
||||||
|
tstest "test/unit/*.ts"
|
||||||
```
|
```
|
||||||
|
|
||||||
to run all tests.
|
### Execution Modes
|
||||||
|
|
||||||
## Syntax
|
**tstest** intelligently detects how you want to run your tests:
|
||||||
|
|
||||||
tstest supports tap syntax. In other words your testfiles are run in a subprocess, and the console output contains trigger messages for tstest to determine test status. Inside your testfile you should use `@pushrocks/tapbundle` for the best results.
|
1. **Directory mode** - Recursively finds all test files
|
||||||
|
2. **File mode** - Runs a single test file
|
||||||
|
3. **Glob mode** - Uses pattern matching for flexible test selection
|
||||||
|
|
||||||
## Environments
|
### Command Line Options
|
||||||
|
|
||||||
tstest supports different environments:
|
| Option | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `--quiet`, `-q` | Minimal output - perfect for CI environments |
|
||||||
|
| `--verbose`, `-v` | Show all console output from tests |
|
||||||
|
| `--no-color` | Disable colored output |
|
||||||
|
| `--json` | Output results as JSON |
|
||||||
|
| `--logfile` | Save detailed logs to `.nogit/testlogs/[testname].log` |
|
||||||
|
| `--tags <tags>` | Run only tests with specific tags (comma-separated) |
|
||||||
|
|
||||||
- a testfile called `test-something.node.ts` will be run in node
|
### Example Outputs
|
||||||
- a testfile called `test-something.chrome.ts` will be run in chrome environment (bundled through parcel and run through puppeteer)
|
|
||||||
- a testfile called `test-something.both.ts` will be run in node an chrome, which is good for isomorphic packages.
|
|
||||||
|
|
||||||
> note: there is alpha support for the deno environment by naming a file test-something.deno.ts
|
#### Normal Output (Default)
|
||||||
|
```
|
||||||
|
🔍 Test Discovery
|
||||||
|
Mode: directory
|
||||||
|
Pattern: test
|
||||||
|
Found: 4 test file(s)
|
||||||
|
|
||||||
|
▶️ test/test.ts (1/4)
|
||||||
|
Runtime: node.js
|
||||||
|
✅ prepare test (1ms)
|
||||||
|
Summary: 1/1 PASSED
|
||||||
|
|
||||||
|
📊 Test Summary
|
||||||
|
┌────────────────────────────────┐
|
||||||
|
│ Total Files: 4 │
|
||||||
|
│ Total Tests: 4 │
|
||||||
|
│ Passed: 4 │
|
||||||
|
│ Failed: 0 │
|
||||||
|
│ Duration: 542ms │
|
||||||
|
└────────────────────────────────┘
|
||||||
|
|
||||||
|
ALL TESTS PASSED! 🎉
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Quiet Mode
|
||||||
|
```
|
||||||
|
Found 4 tests
|
||||||
|
✅ test functionality works
|
||||||
|
✅ api calls return expected data
|
||||||
|
✅ error handling works correctly
|
||||||
|
✅ performance is within limits
|
||||||
|
|
||||||
|
Summary: 4/4 | 542ms | PASSED
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Verbose Mode
|
||||||
|
Shows all console output from your tests, making debugging easier:
|
||||||
|
```
|
||||||
|
▶️ test/api.test.ts (1/1)
|
||||||
|
Runtime: node.js
|
||||||
|
Making API call to /users...
|
||||||
|
Response received: 200 OK
|
||||||
|
Processing user data...
|
||||||
|
✅ api calls return expected data (145ms)
|
||||||
|
Summary: 1/1 PASSED
|
||||||
|
```
|
||||||
|
|
||||||
|
#### JSON Mode
|
||||||
|
Perfect for CI/CD pipelines:
|
||||||
|
```json
|
||||||
|
{"event":"discovery","count":4,"pattern":"test","executionMode":"directory"}
|
||||||
|
{"event":"fileStart","filename":"test/test.ts","runtime":"node.js","index":1,"total":4}
|
||||||
|
{"event":"testResult","testName":"prepare test","passed":true,"duration":1}
|
||||||
|
{"event":"summary","summary":{"totalFiles":4,"totalTests":4,"totalPassed":4,"totalFailed":0,"totalDuration":542}}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test File Naming Conventions
|
||||||
|
|
||||||
|
tstest supports different test environments through file naming:
|
||||||
|
|
||||||
|
| Pattern | Environment | Example |
|
||||||
|
|---------|-------------|---------|
|
||||||
|
| `*.ts` | Node.js (default) | `test.basic.ts` |
|
||||||
|
| `*.node.ts` | Node.js only | `test.api.node.ts` |
|
||||||
|
| `*.chrome.ts` | Chrome browser | `test.dom.chrome.ts` |
|
||||||
|
| `*.browser.ts` | Browser environment | `test.ui.browser.ts` |
|
||||||
|
| `*.both.ts` | Both Node.js and browser | `test.isomorphic.both.ts` |
|
||||||
|
|
||||||
|
### Writing Tests
|
||||||
|
|
||||||
|
tstest includes a built-in TAP (Test Anything Protocol) test framework. Import it from the embedded tapbundle:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
|
|
||||||
|
tap.test('my awesome test', async () => {
|
||||||
|
const result = await myFunction();
|
||||||
|
expect(result).toEqual('expected value');
|
||||||
|
});
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
#### Test Features
|
||||||
|
|
||||||
|
**Tag-based Test Filtering**
|
||||||
|
```typescript
|
||||||
|
tap.tags('unit', 'api')
|
||||||
|
.test('should handle API requests', async () => {
|
||||||
|
// Test code
|
||||||
|
});
|
||||||
|
|
||||||
|
// Run with: tstest test/ --tags unit,api
|
||||||
|
```
|
||||||
|
|
||||||
|
**Test Lifecycle Hooks**
|
||||||
|
```typescript
|
||||||
|
tap.describe('User API Tests', () => {
|
||||||
|
let testUser;
|
||||||
|
|
||||||
|
tap.beforeEach(async () => {
|
||||||
|
testUser = await createTestUser();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.afterEach(async () => {
|
||||||
|
await deleteTestUser(testUser.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should update user profile', async () => {
|
||||||
|
// Test code using testUser
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parallel Test Execution**
|
||||||
|
```typescript
|
||||||
|
// Files with matching parallel group names run concurrently
|
||||||
|
// test.auth.para__1.ts
|
||||||
|
tap.test('authentication test', async () => { /* ... */ });
|
||||||
|
|
||||||
|
// test.user.para__1.ts
|
||||||
|
tap.test('user operations test', async () => { /* ... */ });
|
||||||
|
```
|
||||||
|
|
||||||
|
**Test Timeouts and Retries**
|
||||||
|
```typescript
|
||||||
|
tap.timeout(5000)
|
||||||
|
.retry(3)
|
||||||
|
.test('flaky network test', async (tools) => {
|
||||||
|
// This test has 5 seconds to complete and will retry up to 3 times
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Snapshot Testing**
|
||||||
|
```typescript
|
||||||
|
tap.test('should match snapshot', async (tools) => {
|
||||||
|
const result = await generateReport();
|
||||||
|
await tools.matchSnapshot(result);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Test Fixtures**
|
||||||
|
```typescript
|
||||||
|
// Define a reusable fixture
|
||||||
|
tap.defineFixture('testUser', async () => ({
|
||||||
|
id: 1,
|
||||||
|
name: 'Test User',
|
||||||
|
email: 'test@example.com'
|
||||||
|
}));
|
||||||
|
|
||||||
|
tap.test('user test', async (tools) => {
|
||||||
|
const user = tools.fixture('testUser');
|
||||||
|
expect(user.name).toEqual('Test User');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Skipping and Todo Tests**
|
||||||
|
```typescript
|
||||||
|
tap.skip.test('work in progress', async () => {
|
||||||
|
// This test will be skipped
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.todo('implement user deletion', async () => {
|
||||||
|
// This marks a test as todo
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Browser Testing**
|
||||||
|
```typescript
|
||||||
|
// test.browser.ts
|
||||||
|
import { tap, webhelpers } from '@git.zone/tstest/tapbundle';
|
||||||
|
|
||||||
|
tap.test('DOM manipulation', async () => {
|
||||||
|
const element = await webhelpers.fixture(webhelpers.html`
|
||||||
|
<div>Hello World</div>
|
||||||
|
`);
|
||||||
|
expect(element).toBeInstanceOf(HTMLElement);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Advanced Features
|
||||||
|
|
||||||
|
### Glob Pattern Support
|
||||||
|
|
||||||
|
Run specific test patterns:
|
||||||
|
```bash
|
||||||
|
# Run all unit tests
|
||||||
|
tstest "test/unit/**/*.ts"
|
||||||
|
|
||||||
|
# Run all integration tests
|
||||||
|
tstest "test/integration/*.test.ts"
|
||||||
|
|
||||||
|
# Run multiple patterns
|
||||||
|
tstest "test/**/*.spec.ts" "test/**/*.test.ts"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important**: Always quote glob patterns to prevent shell expansion. Without quotes, the shell will expand the pattern and only pass the first matching file to tstest.
|
||||||
|
|
||||||
|
### Automatic Logging
|
||||||
|
|
||||||
|
Use `--logfile` to automatically save test output:
|
||||||
|
```bash
|
||||||
|
tstest test/ --logfile
|
||||||
|
```
|
||||||
|
|
||||||
|
This creates detailed logs in `.nogit/testlogs/[testname].log` for each test file.
|
||||||
|
|
||||||
|
### Performance Analysis
|
||||||
|
|
||||||
|
In verbose mode, see performance metrics:
|
||||||
|
```
|
||||||
|
⏱️ Performance Metrics:
|
||||||
|
Average per test: 135ms
|
||||||
|
Slowest test: api integration test (486ms)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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:
|
||||||
|
```bash
|
||||||
|
# GitHub Actions example
|
||||||
|
tstest test/ --json > test-results.json
|
||||||
|
|
||||||
|
# Or minimal output
|
||||||
|
tstest test/ --quiet
|
||||||
|
```
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
### 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
|
||||||
|
|
||||||
## Contribution
|
## Contribution
|
||||||
|
|
||||||
We are always happy for code contributions. If you are not the code contributing type that is ok. Still, maintaining Open Source repositories takes considerable time and thought. If you like the quality of what we do and our modules are useful to you we would appreciate a little monthly contribution: You can [contribute one time](https://lossless.link/contribute-onetime) or [contribute monthly](https://lossless.link/contribute). :)
|
We are always happy for code contributions. If you are not the code contributing type that is ok. Still, maintaining Open Source repositories takes considerable time and thought. If you like the quality of what we do and our modules are useful to you we would appreciate a little monthly contribution: You can [contribute one time](https://lossless.link/contribute-onetime) or [contribute monthly](https://lossless.link/contribute). :)
|
||||||
|
|
||||||
For further information read the linked docs at the top of this readme.
|
## License
|
||||||
|
|
||||||
> MIT licensed | **©** [Lossless GmbH](https://lossless.gmbh)
|
> MIT licensed | **©** [Lossless GmbH](https://lossless.gmbh)
|
||||||
| By using this npm module you agree to our [privacy policy](https://lossless.gmbH/privacy)
|
| By using this npm module you agree to our [privacy policy](https://lossless.gmbH/privacy)
|
||||||
|
264
readme.plan.md
Normal file
264
readme.plan.md
Normal file
@ -0,0 +1,264 @@
|
|||||||
|
# Improvement Plan for tstest and tapbundle
|
||||||
|
|
||||||
|
!! FIRST: Reread /home/philkunz/.claude/CLAUDE.md to ensure following all guidelines !!
|
||||||
|
|
||||||
|
## 1. Enhanced Communication Between tapbundle and tstest
|
||||||
|
|
||||||
|
### 1.1 Real-time Test Progress API
|
||||||
|
- Create a bidirectional communication channel between tapbundle and tstest
|
||||||
|
- Emit events for test lifecycle stages (start, progress, completion)
|
||||||
|
- Allow tstest to subscribe to tapbundle events for better progress reporting
|
||||||
|
- Implement a standardized message format for test metadata
|
||||||
|
|
||||||
|
### 1.2 Rich Error Reporting
|
||||||
|
- Pass structured error objects from tapbundle to tstest
|
||||||
|
- Include stack traces, code snippets, and contextual information
|
||||||
|
- Support for error categorization (assertion failures, timeouts, uncaught exceptions)
|
||||||
|
- Visual diff output for failed assertions
|
||||||
|
|
||||||
|
## 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
|
||||||
|
// Fluent syntax ✅
|
||||||
|
tap.tags('slow', 'integration')
|
||||||
|
.priority('high')
|
||||||
|
.timeout(5000)
|
||||||
|
.retry(3)
|
||||||
|
.test('configurable test', async (toolsArg) => {
|
||||||
|
// Test implementation
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 Test Data and Context Sharing ✅
|
||||||
|
```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 (not yet implemented)
|
||||||
|
const testData = toolsArg.data<TestInput>();
|
||||||
|
expect(processData(testData)).toEqual(expected);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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
|
||||||
|
- Support for multiple levels of nesting
|
||||||
|
- Inherited context and configuration from parent suites
|
||||||
|
- Aggregated reporting for test suites
|
||||||
|
- Suite-level lifecycle hooks
|
||||||
|
|
||||||
|
## 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.2 Performance Benchmarking
|
||||||
|
```typescript
|
||||||
|
tap.test('performance test', async (toolsArg) => {
|
||||||
|
const benchmark = toolsArg.benchmark();
|
||||||
|
|
||||||
|
// Run operation
|
||||||
|
await expensiveOperation();
|
||||||
|
|
||||||
|
// Assert performance constraints
|
||||||
|
benchmark.expect({
|
||||||
|
maxDuration: 1000,
|
||||||
|
maxMemory: '100MB'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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 (via file naming convention)
|
||||||
|
- Resource pooling for shared resources
|
||||||
|
- Proper isolation between parallel tests ✅
|
||||||
|
|
||||||
|
Implementation:
|
||||||
|
- Tests with `para__<groupNumber>` in filename run in parallel
|
||||||
|
- Different groups run sequentially
|
||||||
|
- Tests without `para__` run serially
|
||||||
|
|
||||||
|
### 5.2 Watch Mode
|
||||||
|
- Automatically re-run tests on file changes
|
||||||
|
- Intelligent test selection based on changed files
|
||||||
|
- Fast feedback loop for development
|
||||||
|
- Integration with IDE/editor plugins
|
||||||
|
|
||||||
|
### 5.3 Advanced Test Filtering ✅ (partially)
|
||||||
|
```typescript
|
||||||
|
// Run tests by tags ✅
|
||||||
|
tstest --tags "unit,fast"
|
||||||
|
|
||||||
|
// Exclude tests by pattern (not yet implemented)
|
||||||
|
tstest --exclude "**/slow/**"
|
||||||
|
|
||||||
|
// Run only failed tests from last run (not yet implemented)
|
||||||
|
tstest --failed
|
||||||
|
|
||||||
|
// Run tests modified in git (not yet implemented)
|
||||||
|
tstest --changed
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Reporting and Analytics
|
||||||
|
|
||||||
|
### 6.1 Custom Reporters
|
||||||
|
- Plugin architecture for custom reporters
|
||||||
|
- Built-in reporters: JSON, JUnit, HTML, Markdown
|
||||||
|
- Real-time streaming reporters
|
||||||
|
- Aggregated test metrics and trends
|
||||||
|
|
||||||
|
### 6.2 Coverage Integration
|
||||||
|
- Built-in code coverage collection
|
||||||
|
- Coverage thresholds and enforcement
|
||||||
|
- Coverage trending over time
|
||||||
|
- Integration with CI/CD pipelines
|
||||||
|
|
||||||
|
### 6.3 Test Analytics Dashboard
|
||||||
|
- Web-based dashboard for test results
|
||||||
|
- Historical test performance data
|
||||||
|
- Flaky test detection
|
||||||
|
- Test impact analysis
|
||||||
|
|
||||||
|
## 7. Developer Experience
|
||||||
|
|
||||||
|
### 7.1 Better Error Messages
|
||||||
|
- Clear, actionable error messages
|
||||||
|
- Suggestions for common issues
|
||||||
|
- Links to documentation
|
||||||
|
- Code examples in error output
|
||||||
|
|
||||||
|
### 7.2 Interactive Mode (Needs Detailed Specification)
|
||||||
|
- REPL for exploring test failures
|
||||||
|
- Need to define: How to enter interactive mode? When tests fail?
|
||||||
|
- What commands/features should be available in the REPL?
|
||||||
|
- Debugging integration
|
||||||
|
- Node.js inspector protocol integration?
|
||||||
|
- Breakpoint support?
|
||||||
|
- Step-through test execution
|
||||||
|
- Pause between tests?
|
||||||
|
- Step into/over/out functionality?
|
||||||
|
- Interactive test data manipulation
|
||||||
|
- Modify test inputs on the fly?
|
||||||
|
- Inspect intermediate values?
|
||||||
|
|
||||||
|
### 7.3 ~~VS Code Extension~~ (Scratched)
|
||||||
|
- ~~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 2: Advanced Features (Priority: Medium)
|
||||||
|
1. Implement nested test suites ✅ (basic describe support)
|
||||||
|
2. Add snapshot testing ✅
|
||||||
|
3. Create test fixture system ✅
|
||||||
|
4. Implement parallel test execution ✅
|
||||||
|
|
||||||
|
### Phase 3: Developer Experience (Priority: Medium)
|
||||||
|
1. Add watch mode
|
||||||
|
2. Implement custom reporters
|
||||||
|
3. ~~Create VS Code extension~~ (Scratched)
|
||||||
|
4. Add interactive debugging (Needs detailed spec first)
|
||||||
|
|
||||||
|
### Phase 4: Analytics and Performance (Priority: Low)
|
||||||
|
1. Build test analytics dashboard
|
||||||
|
2. Add performance benchmarking
|
||||||
|
3. Implement coverage integration
|
||||||
|
4. Create trend analysis tools
|
||||||
|
|
||||||
|
## Technical Considerations
|
||||||
|
|
||||||
|
### API Design Principles
|
||||||
|
- Maintain backward compatibility
|
||||||
|
- Progressive enhancement approach
|
||||||
|
- Opt-in features to avoid breaking changes
|
||||||
|
- Clear migration paths for new features
|
||||||
|
|
||||||
|
### Performance Goals
|
||||||
|
- Minimal overhead for test execution
|
||||||
|
- Efficient parallel execution
|
||||||
|
- Fast test discovery
|
||||||
|
- Optimized browser test bundling
|
||||||
|
|
||||||
|
### Integration Points
|
||||||
|
- Clean interfaces between tstest and tapbundle
|
||||||
|
- Extensible plugin architecture
|
||||||
|
- Standard test result format
|
||||||
|
- Compatible with existing CI/CD tools
|
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());
|
55
test/tapbundle/test.browser.nonci.ts
Normal file
55
test/tapbundle/test.browser.nonci.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { tap, expect, webhelpers } from '../../ts_tapbundle/index.js';
|
||||||
|
|
||||||
|
tap.preTask('custompretask', async () => {
|
||||||
|
console.log('this is a pretask');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should have access to webhelpers', async () => {
|
||||||
|
const myElement = await webhelpers.fixture(webhelpers.html`<div></div>`);
|
||||||
|
expect(myElement).toBeInstanceOf(HTMLElement);
|
||||||
|
console.log(myElement);
|
||||||
|
});
|
||||||
|
|
||||||
|
const test1 = tap.test('my first test -> expect true to be true', async () => {
|
||||||
|
return expect(true).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
const test2 = tap.test('my second test', async (tools) => {
|
||||||
|
await tools.delayFor(50);
|
||||||
|
});
|
||||||
|
|
||||||
|
const test3 = tap.test(
|
||||||
|
'my third test -> test2 should take longer than test1 and endure at least 1000ms',
|
||||||
|
async () => {
|
||||||
|
expect(
|
||||||
|
(await test1.testPromise).hrtMeasurement.milliSeconds <
|
||||||
|
(await test2).hrtMeasurement.milliSeconds,
|
||||||
|
).toBeTrue();
|
||||||
|
expect((await test2.testPromise).hrtMeasurement.milliSeconds > 10).toBeTrue();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const test4 = tap.skip.test('my 4th test -> should fail', async (tools) => {
|
||||||
|
tools.allowFailure();
|
||||||
|
expect(false).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
const test5 = tap.test('my 5th test -> should pass in about 500ms', async (tools) => {
|
||||||
|
tools.timeout(1000);
|
||||||
|
await tools.delayFor(500);
|
||||||
|
});
|
||||||
|
|
||||||
|
const test6 = tap.skip.test('my 6th test -> should fail after 1000ms', async (tools) => {
|
||||||
|
tools.allowFailure();
|
||||||
|
tools.timeout(1000);
|
||||||
|
await tools.delayFor(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
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();
|
28
test/tapbundle/test.node.ts
Normal file
28
test/tapbundle/test.node.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { tap, expect } from '../../ts_tapbundle/index.js';
|
||||||
|
|
||||||
|
import { tapNodeTools } from '../../ts_tapbundle_node/index.js';
|
||||||
|
|
||||||
|
tap.test('should execure a command', async () => {
|
||||||
|
const result = await tapNodeTools.runCommand('ls -la');
|
||||||
|
expect(result.exitCode).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should create a https cert', async () => {
|
||||||
|
const { key, cert } = await tapNodeTools.createHttpsCert('localhost');
|
||||||
|
console.log(key);
|
||||||
|
console.log(cert);
|
||||||
|
expect(key).toInclude('-----BEGIN RSA PRIVATE KEY-----');
|
||||||
|
expect(cert).toInclude('-----BEGIN CERTIFICATE-----');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should create a smartmongo instance', async () => {
|
||||||
|
const smartmongo = await tapNodeTools.createSmartmongo();
|
||||||
|
await smartmongo.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should create a smarts3 instance', async () => {
|
||||||
|
const smarts3 = await tapNodeTools.createSmarts3();
|
||||||
|
await smarts3.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.start();
|
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();
|
5
test/tapbundle/test.tapwrap.ts
Normal file
5
test/tapbundle/test.tapwrap.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { tap, expect, TapWrap } from '../../ts_tapbundle/index.js';
|
||||||
|
|
||||||
|
tap.test('should run a test', async () => {});
|
||||||
|
|
||||||
|
tap.start();
|
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();
|
49
test/tapbundle/test.ts
Normal file
49
test/tapbundle/test.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { tap, expect } from '../../ts_tapbundle/index.js';
|
||||||
|
|
||||||
|
tap.preTask('hi there', async () => {
|
||||||
|
console.log('this is a pretask');
|
||||||
|
});
|
||||||
|
|
||||||
|
const test1 = tap.test('my first test -> expect true to be true', async () => {
|
||||||
|
return expect(true).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
const test2 = tap.test('my second test', async (tools) => {
|
||||||
|
await tools.delayFor(1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
const test3 = tap.test(
|
||||||
|
'my third test -> test2 should take longer than test1 and endure at least 1000ms',
|
||||||
|
async () => {
|
||||||
|
expect(
|
||||||
|
(await test1.testPromise).hrtMeasurement.milliSeconds <
|
||||||
|
(await test2.testPromise).hrtMeasurement.milliSeconds,
|
||||||
|
).toBeTrue();
|
||||||
|
expect((await test2.testPromise).hrtMeasurement.milliSeconds >= 1000).toBeTrue();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const test4 = tap.test('my 4th test -> should fail', async (tools) => {
|
||||||
|
tools.allowFailure();
|
||||||
|
expect(false).toBeFalse();
|
||||||
|
return 'hello';
|
||||||
|
});
|
||||||
|
|
||||||
|
const test5 = tap.test('my 5th test -> should pass in about 500ms', async (tools) => {
|
||||||
|
const test4Result = await test4.testResultPromise;
|
||||||
|
tools.timeout(1000);
|
||||||
|
await tools.delayFor(500);
|
||||||
|
});
|
||||||
|
|
||||||
|
const test6 = tap.skip.test('my 6th test -> should fail after 1000ms', async (tools) => {
|
||||||
|
tools.allowFailure();
|
||||||
|
tools.timeout(1000);
|
||||||
|
await tools.delayFor(2000);
|
||||||
|
});
|
||||||
|
|
||||||
|
const test7 = tap.test('my 7th test -> should print a colored string', async (tools) => {
|
||||||
|
const cs = await tools.coloredString('hello', 'red', 'cyan');
|
||||||
|
console.log(cs);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.start();
|
@ -1,6 +0,0 @@
|
|||||||
import { expect, tap } from '@push.rocks/tapbundle';
|
|
||||||
import * as tstest from '../ts/index.js';
|
|
||||||
|
|
||||||
tap.test('prepare test', async () => {});
|
|
||||||
|
|
||||||
tap.start();
|
|
8
test/tstest/subdir/test.sub.ts
Normal file
8
test/tstest/subdir/test.sub.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { expect, tap } from '../../../ts_tapbundle/index.js';
|
||||||
|
|
||||||
|
tap.test('subdirectory test execution', async () => {
|
||||||
|
console.log('This test verifies subdirectory test discovery works');
|
||||||
|
expect(true).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.start();
|
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.console.ts
Normal file
11
test/tstest/test.console.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { expect, tap } from '../../ts_tapbundle/index.js';
|
||||||
|
|
||||||
|
tap.test('Test with console output', async () => {
|
||||||
|
console.log('Log message 1 from test');
|
||||||
|
console.log('Log message 2 from test');
|
||||||
|
console.error('Error message from test');
|
||||||
|
console.warn('Warning message from test');
|
||||||
|
expect(true).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.start();
|
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();
|
13
test/tstest/test.fail.ts
Normal file
13
test/tstest/test.fail.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { expect, tap } from '../../ts_tapbundle/index.js';
|
||||||
|
|
||||||
|
tap.test('This test should fail', async () => {
|
||||||
|
console.log('This test will fail on purpose');
|
||||||
|
expect(true).toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('This test should pass', async () => {
|
||||||
|
console.log('This test will pass');
|
||||||
|
expect(true).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.start();
|
23
test/tstest/test.failing-with-logs.ts
Normal file
23
test/tstest/test.failing-with-logs.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { expect, tap } from '../../ts_tapbundle/index.js';
|
||||||
|
|
||||||
|
tap.test('Test that will fail with console logs', async () => {
|
||||||
|
console.log('Starting the test...');
|
||||||
|
console.log('Doing some setup work');
|
||||||
|
console.log('About to check assertion');
|
||||||
|
|
||||||
|
const value = 42;
|
||||||
|
console.log(`The value is: ${value}`);
|
||||||
|
|
||||||
|
// This will fail
|
||||||
|
expect(value).toEqual(100);
|
||||||
|
|
||||||
|
console.log('This log will not be reached');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('Test that passes', async () => {
|
||||||
|
console.log('This test passes');
|
||||||
|
console.log('So these logs should not show in default mode');
|
||||||
|
expect(true).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.start();
|
8
test/tstest/test.glob.ts
Normal file
8
test/tstest/test.glob.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { expect, tap } from '../../ts_tapbundle/index.js';
|
||||||
|
|
||||||
|
tap.test('glob pattern test execution', async () => {
|
||||||
|
console.log('This test verifies glob pattern execution works');
|
||||||
|
expect(true).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.start();
|
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();
|
8
test/tstest/test.single.ts
Normal file
8
test/tstest/test.single.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { expect, tap } from '../../ts_tapbundle/index.js';
|
||||||
|
|
||||||
|
tap.test('single file test execution', async () => {
|
||||||
|
console.log('This test verifies single file execution works');
|
||||||
|
expect(true).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.start();
|
6
test/tstest/test.ts
Normal file
6
test/tstest/test.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { expect, tap } from '../../ts_tapbundle/index.js';
|
||||||
|
import * as tstest from '../../ts/index.js';
|
||||||
|
|
||||||
|
tap.test('prepare test', async () => {});
|
||||||
|
|
||||||
|
tap.start();
|
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();
|
@ -1,8 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* autocreated commitinfo by @pushrocks/commitinfo
|
* autocreated commitinfo by @push.rocks/commitinfo
|
||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@git.zone/tstest',
|
name: '@git.zone/tstest',
|
||||||
version: '1.0.80',
|
version: '1.9.0',
|
||||||
description: 'a test utility to run tests that match test/**/*.ts'
|
description: 'a test utility to run tests that match test/**/*.ts'
|
||||||
}
|
}
|
||||||
|
74
ts/index.ts
74
ts/index.ts
@ -1,10 +1,78 @@
|
|||||||
import { TsTest } from './tstest.classes.tstest.js';
|
import { TsTest } from './tstest.classes.tstest.js';
|
||||||
|
import type { LogOptions } from './tstest.logging.js';
|
||||||
|
|
||||||
|
export enum TestExecutionMode {
|
||||||
|
DIRECTORY = 'directory',
|
||||||
|
FILE = 'file',
|
||||||
|
GLOB = 'glob'
|
||||||
|
}
|
||||||
|
|
||||||
export const runCli = async () => {
|
export const runCli = async () => {
|
||||||
if (!process.argv[2]) {
|
// Parse command line arguments
|
||||||
console.error('You must specify a test directory as argument. Please try again.');
|
const args = process.argv.slice(2);
|
||||||
|
const logOptions: LogOptions = {};
|
||||||
|
let testPath: string | null = null;
|
||||||
|
let tags: string[] = [];
|
||||||
|
|
||||||
|
// Parse options
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i];
|
||||||
|
|
||||||
|
switch (arg) {
|
||||||
|
case '--quiet':
|
||||||
|
case '-q':
|
||||||
|
logOptions.quiet = true;
|
||||||
|
break;
|
||||||
|
case '--verbose':
|
||||||
|
case '-v':
|
||||||
|
logOptions.verbose = true;
|
||||||
|
break;
|
||||||
|
case '--no-color':
|
||||||
|
logOptions.noColor = true;
|
||||||
|
break;
|
||||||
|
case '--json':
|
||||||
|
logOptions.json = true;
|
||||||
|
break;
|
||||||
|
case '--log-file':
|
||||||
|
case '--logfile':
|
||||||
|
logOptions.logFile = true; // Set this as a flag, not a value
|
||||||
|
break;
|
||||||
|
case '--tags':
|
||||||
|
if (i + 1 < args.length) {
|
||||||
|
tags = args[++i].split(',');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (!arg.startsWith('-')) {
|
||||||
|
testPath = arg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!testPath) {
|
||||||
|
console.error('You must specify a test directory/file/pattern as argument. Please try again.');
|
||||||
|
console.error('\nUsage: tstest <path> [options]');
|
||||||
|
console.error('\nOptions:');
|
||||||
|
console.error(' --quiet, -q Minimal output');
|
||||||
|
console.error(' --verbose, -v Verbose output');
|
||||||
|
console.error(' --no-color Disable colored output');
|
||||||
|
console.error(' --json Output results as JSON');
|
||||||
|
console.error(' --logfile Write logs to .nogit/testlogs/[testfile].log');
|
||||||
|
console.error(' --tags Run only tests with specified tags (comma-separated)');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
const tsTestInstance = new TsTest(process.cwd(), process.argv[2]);
|
|
||||||
|
let executionMode: TestExecutionMode;
|
||||||
|
|
||||||
|
// Detect execution mode based on the argument
|
||||||
|
if (testPath.includes('*') || testPath.includes('?') || testPath.includes('[') || testPath.includes('{')) {
|
||||||
|
executionMode = TestExecutionMode.GLOB;
|
||||||
|
} else if (testPath.endsWith('.ts')) {
|
||||||
|
executionMode = TestExecutionMode.FILE;
|
||||||
|
} else {
|
||||||
|
executionMode = TestExecutionMode.DIRECTORY;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tsTestInstance = new TsTest(process.cwd(), testPath, executionMode, logOptions, tags);
|
||||||
await tsTestInstance.run();
|
await tsTestInstance.run();
|
||||||
};
|
};
|
||||||
|
3
ts/tspublish.json
Normal file
3
ts/tspublish.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"order": 2
|
||||||
|
}
|
@ -6,59 +6,37 @@ import { coloredString as cs } from '@push.rocks/consolecolor';
|
|||||||
|
|
||||||
import { TapParser } from './tstest.classes.tap.parser.js';
|
import { TapParser } from './tstest.classes.tap.parser.js';
|
||||||
import * as logPrefixes from './tstest.logprefixes.js';
|
import * as logPrefixes from './tstest.logprefixes.js';
|
||||||
|
import { TsTestLogger } from './tstest.logging.js';
|
||||||
|
|
||||||
export class TapCombinator {
|
export class TapCombinator {
|
||||||
tapParserStore: TapParser[] = [];
|
tapParserStore: TapParser[] = [];
|
||||||
|
private logger: TsTestLogger;
|
||||||
|
|
||||||
|
constructor(logger: TsTestLogger) {
|
||||||
|
this.logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
addTapParser(tapParserArg: TapParser) {
|
addTapParser(tapParserArg: TapParser) {
|
||||||
this.tapParserStore.push(tapParserArg);
|
this.tapParserStore.push(tapParserArg);
|
||||||
}
|
}
|
||||||
|
|
||||||
evaluate() {
|
evaluate() {
|
||||||
console.log(
|
// Call the logger's summary method
|
||||||
`${logPrefixes.TsTestPrefix} RESULTS FOR ${this.tapParserStore.length} TESTFILE(S):`
|
this.logger.summary();
|
||||||
);
|
|
||||||
|
|
||||||
let failGlobal = false; // determine wether tstest should fail
|
// Check for failures
|
||||||
|
let failGlobal = false;
|
||||||
for (const tapParser of this.tapParserStore) {
|
for (const tapParser of this.tapParserStore) {
|
||||||
if (!tapParser.expectedTests) {
|
if (!tapParser.expectedTests ||
|
||||||
|
tapParser.expectedTests !== tapParser.receivedTests ||
|
||||||
|
tapParser.getErrorTests().length > 0) {
|
||||||
failGlobal = true;
|
failGlobal = true;
|
||||||
let overviewString =
|
break;
|
||||||
logPrefixes.TsTestPrefix +
|
|
||||||
cs(` ${tapParser.fileName} ${plugins.figures.cross}`, 'red') +
|
|
||||||
` ${plugins.figures.pointer} ` +
|
|
||||||
`does not specify tests!`;
|
|
||||||
console.log(overviewString);
|
|
||||||
} else if (tapParser.expectedTests !== tapParser.receivedTests) {
|
|
||||||
failGlobal = true;
|
|
||||||
let overviewString =
|
|
||||||
logPrefixes.TsTestPrefix +
|
|
||||||
cs(` ${tapParser.fileName} ${plugins.figures.cross}`, 'red') +
|
|
||||||
` ${plugins.figures.pointer} ` +
|
|
||||||
tapParser.getTestOverviewAsString() +
|
|
||||||
`did not execute all specified tests!`;
|
|
||||||
console.log(overviewString);
|
|
||||||
} else if (tapParser.getErrorTests().length === 0) {
|
|
||||||
let overviewString =
|
|
||||||
logPrefixes.TsTestPrefix +
|
|
||||||
cs(` ${tapParser.fileName} ${plugins.figures.tick}`, 'green') +
|
|
||||||
` ${plugins.figures.pointer} ` +
|
|
||||||
tapParser.getTestOverviewAsString();
|
|
||||||
console.log(overviewString);
|
|
||||||
} else {
|
|
||||||
failGlobal = true;
|
|
||||||
let overviewString =
|
|
||||||
logPrefixes.TsTestPrefix +
|
|
||||||
cs(` ${tapParser.fileName} ${plugins.figures.cross}`, 'red') +
|
|
||||||
` ${plugins.figures.pointer} ` +
|
|
||||||
tapParser.getTestOverviewAsString();
|
|
||||||
console.log(overviewString);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log(cs(plugins.figures.hamburger.repeat(48), 'cyan'));
|
|
||||||
if (!failGlobal) {
|
// Exit with error code if tests failed
|
||||||
console.log(cs('FINAL RESULT: SUCCESS!', 'green'));
|
if (failGlobal) {
|
||||||
} else {
|
|
||||||
console.log(cs('FINAL RESULT: FAIL!', 'red'));
|
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import { coloredString as cs } from '@push.rocks/consolecolor';
|
|||||||
import * as plugins from './tstest.plugins.js';
|
import * as plugins from './tstest.plugins.js';
|
||||||
import { TapTestResult } from './tstest.classes.tap.testresult.js';
|
import { TapTestResult } from './tstest.classes.tap.testresult.js';
|
||||||
import * as logPrefixes from './tstest.logprefixes.js';
|
import * as logPrefixes from './tstest.logprefixes.js';
|
||||||
|
import { TsTestLogger } from './tstest.logging.js';
|
||||||
|
|
||||||
export class TapParser {
|
export class TapParser {
|
||||||
testStore: TapTestResult[] = [];
|
testStore: TapTestResult[] = [];
|
||||||
@ -15,13 +16,21 @@ export class TapParser {
|
|||||||
expectedTests: number;
|
expectedTests: number;
|
||||||
receivedTests: 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;
|
activeTapTestResult: TapTestResult;
|
||||||
|
collectingErrorDetails: boolean = false;
|
||||||
|
currentTestError: string[] = [];
|
||||||
|
|
||||||
|
pretaskRegex = /^::__PRETASK:(.*)$/;
|
||||||
|
|
||||||
|
private logger: TsTestLogger;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* the constructor for TapParser
|
* the constructor for TapParser
|
||||||
*/
|
*/
|
||||||
constructor(public fileName: string) {}
|
constructor(public fileName: string, logger?: TsTestLogger) {
|
||||||
|
this.logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
private _getNewTapTestResult() {
|
private _getNewTapTestResult() {
|
||||||
this.activeTapTestResult = new TapTestResult(this.testStore.length + 1);
|
this.activeTapTestResult = new TapTestResult(this.testStore.length + 1);
|
||||||
@ -43,12 +52,20 @@ export class TapParser {
|
|||||||
logLineIsTapProtocol = true;
|
logLineIsTapProtocol = true;
|
||||||
const regexResult = this.expectedTestsRegex.exec(logLine);
|
const regexResult = this.expectedTestsRegex.exec(logLine);
|
||||||
this.expectedTests = parseInt(regexResult[2]);
|
this.expectedTests = parseInt(regexResult[2]);
|
||||||
console.log(
|
if (this.logger) {
|
||||||
`${logPrefixes.TapPrefix} ${cs(`Expecting ${this.expectedTests} tests!`, 'blue')}`
|
this.logger.tapOutput(`Expecting ${this.expectedTests} tests!`);
|
||||||
);
|
}
|
||||||
|
|
||||||
// initiating first TapResult
|
// initiating first TapResult
|
||||||
this._getNewTapTestResult();
|
this._getNewTapTestResult();
|
||||||
|
} else if (this.pretaskRegex.test(logLine)) {
|
||||||
|
logLineIsTapProtocol = true;
|
||||||
|
const pretaskContentMatch = this.pretaskRegex.exec(logLine);
|
||||||
|
if (pretaskContentMatch && pretaskContentMatch[1]) {
|
||||||
|
if (this.logger) {
|
||||||
|
this.logger.tapOutput(`Pretask -> ${pretaskContentMatch[1]}: Success.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (this.testStatusRegex.test(logLine)) {
|
} else if (this.testStatusRegex.test(logLine)) {
|
||||||
logLineIsTapProtocol = true;
|
logLineIsTapProtocol = true;
|
||||||
const regexResult = this.testStatusRegex.exec(logLine);
|
const regexResult = this.testStatusRegex.exec(logLine);
|
||||||
@ -61,30 +78,46 @@ export class TapParser {
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
const testSubject = regexResult[3];
|
const testSubject = regexResult[3];
|
||||||
const testDuration = parseInt(regexResult[4]);
|
const testMetadata = regexResult[5]; // This will be either "time=XXXms" or "SKIP reason" or "TODO reason"
|
||||||
|
|
||||||
// test for protocol error
|
let testDuration = 0;
|
||||||
if (testId !== this.activeTapTestResult.id) {
|
let isSkipped = false;
|
||||||
console.log(
|
let isTodo = false;
|
||||||
`${logPrefixes.TapErrorPrefix} Something is strange! Test Ids are not equal!`
|
|
||||||
);
|
if (testMetadata) {
|
||||||
|
const timeMatch = testMetadata.match(/time=(\d+)ms/);
|
||||||
|
const skipMatch = testMetadata.match(/SKIP\s*(.*)/);
|
||||||
|
const todoMatch = testMetadata.match(/TODO\s*(.*)/);
|
||||||
|
|
||||||
|
if (timeMatch) {
|
||||||
|
testDuration = parseInt(timeMatch[1]);
|
||||||
|
} else if (skipMatch) {
|
||||||
|
isSkipped = true;
|
||||||
|
} else if (todoMatch) {
|
||||||
|
isTodo = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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);
|
this.activeTapTestResult.setTestResult(testOk);
|
||||||
|
|
||||||
if (testOk) {
|
if (testOk) {
|
||||||
console.log(
|
if (this.logger) {
|
||||||
logPrefixes.TapPrefix,
|
this.logger.testResult(testSubject, true, testDuration);
|
||||||
`${cs(`T${testId} ${plugins.figures.tick}`, 'green')} ${plugins.figures.arrowRight} ` +
|
}
|
||||||
cs(testSubject, 'blue') +
|
|
||||||
` | ${cs(`${testDuration} ms`, 'orange')}`
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
console.log(
|
// Start collecting error details for failed test
|
||||||
logPrefixes.TapPrefix,
|
this.collectingErrorDetails = true;
|
||||||
`${cs(`T${testId} ${plugins.figures.cross}`, 'red')} ${plugins.figures.arrowRight} ` +
|
this.currentTestError = [];
|
||||||
cs(testSubject, 'blue') +
|
if (this.logger) {
|
||||||
` | ${cs(`${testDuration} ms`, 'orange')}`
|
this.logger.testResult(testSubject, false, testDuration);
|
||||||
);
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,10 +125,57 @@ export class TapParser {
|
|||||||
if (this.activeTapTestResult) {
|
if (this.activeTapTestResult) {
|
||||||
this.activeTapTestResult.addLogLine(logLine);
|
this.activeTapTestResult.addLogLine(logLine);
|
||||||
}
|
}
|
||||||
console.log(logLine);
|
|
||||||
|
// 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.testConsoleOutput(`Error parsing snapshot data: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.activeTapTestResult && this.activeTapTestResult.testSettled) {
|
if (this.activeTapTestResult && this.activeTapTestResult.testSettled) {
|
||||||
|
// Ensure any pending error is shown before settling the test
|
||||||
|
if (this.collectingErrorDetails && this.currentTestError.length > 0) {
|
||||||
|
const errorMessage = this.currentTestError.join('\n');
|
||||||
|
if (this.logger) {
|
||||||
|
this.logger.testErrorDetails(errorMessage);
|
||||||
|
}
|
||||||
|
this.collectingErrorDetails = false;
|
||||||
|
this.currentTestError = [];
|
||||||
|
}
|
||||||
|
|
||||||
this.testStore.push(this.activeTapTestResult);
|
this.testStore.push(this.activeTapTestResult);
|
||||||
this._getNewTapTestResult();
|
this._getNewTapTestResult();
|
||||||
}
|
}
|
||||||
@ -149,7 +229,7 @@ export class TapParser {
|
|||||||
this._processLog(data);
|
this._processLog(data);
|
||||||
});
|
});
|
||||||
childProcessArg.on('exit', async () => {
|
childProcessArg.on('exit', async () => {
|
||||||
await this._evaluateResult();
|
await this.evaluateFinalResult();
|
||||||
done.resolve();
|
done.resolve();
|
||||||
});
|
});
|
||||||
await done.promise;
|
await done.promise;
|
||||||
@ -157,46 +237,92 @@ export class TapParser {
|
|||||||
|
|
||||||
public async handleTapLog(tapLog: string) {
|
public async handleTapLog(tapLog: string) {
|
||||||
this._processLog(tapLog);
|
this._processLog(tapLog);
|
||||||
await this._evaluateResult();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _evaluateResult() {
|
/**
|
||||||
|
* 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;
|
this.receivedTests = this.testStore.length;
|
||||||
|
|
||||||
// check wether all tests ran
|
// check wether all tests ran
|
||||||
if (this.expectedTests === this.receivedTests) {
|
if (this.expectedTests === this.receivedTests) {
|
||||||
console.log(
|
if (this.logger) {
|
||||||
`${logPrefixes.TapPrefix} ${cs(
|
this.logger.tapOutput(`${this.receivedTests} out of ${this.expectedTests} Tests completed!`);
|
||||||
`${this.receivedTests} out of ${this.expectedTests} Tests completed!`,
|
}
|
||||||
'green'
|
|
||||||
)}`
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
console.log(
|
if (this.logger) {
|
||||||
`${logPrefixes.TapErrorPrefix} ${cs(
|
this.logger.error(`Only ${this.receivedTests} out of ${this.expectedTests} completed!`);
|
||||||
`Only ${this.receivedTests} out of ${this.expectedTests} completed!`,
|
}
|
||||||
'red'
|
|
||||||
)}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (!this.expectedTests) {
|
if (!this.expectedTests) {
|
||||||
console.log(cs('Error: No tests were defined. Therefore the testfile failed!', 'red'));
|
if (this.logger) {
|
||||||
|
this.logger.error('No tests were defined. Therefore the testfile failed!');
|
||||||
|
}
|
||||||
} else if (this.expectedTests !== this.receivedTests) {
|
} else if (this.expectedTests !== this.receivedTests) {
|
||||||
console.log(
|
if (this.logger) {
|
||||||
cs(
|
this.logger.error('The amount of received tests and expectedTests is unequal! Therefore the testfile failed');
|
||||||
'Error: The amount of received tests and expectedTests is unequal! Therefore the testfile failed',
|
}
|
||||||
'red'
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else if (this.getErrorTests().length === 0) {
|
} else if (this.getErrorTests().length === 0) {
|
||||||
console.log(`${logPrefixes.TapPrefix} ${cs(`All tests are successfull!!!`, 'green')}`);
|
if (this.logger) {
|
||||||
|
this.logger.tapOutput('All tests are successfull!!!');
|
||||||
|
this.logger.testFileEnd(this.receivedTests, 0, 0);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log(
|
if (this.logger) {
|
||||||
`${logPrefixes.TapPrefix} ${cs(
|
this.logger.tapOutput(`${this.getErrorTests().length} tests threw an error!!!`, true);
|
||||||
`${this.getErrorTests().length} tests threw an error!!!`,
|
this.logger.testFileEnd(this.receivedTests - this.getErrorTests().length, this.getErrorTests().length, 0);
|
||||||
'red'
|
}
|
||||||
)}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import * as plugins from './tstest.plugins.js';
|
import * as plugins from './tstest.plugins.js';
|
||||||
import * as paths from './tstest.paths.js';
|
import * as paths from './tstest.paths.js';
|
||||||
import { Smartfile } from '@push.rocks/smartfile';
|
import { SmartFile } from '@push.rocks/smartfile';
|
||||||
|
import { TestExecutionMode } from './index.js';
|
||||||
|
|
||||||
// tap related stuff
|
// tap related stuff
|
||||||
import { TapCombinator } from './tstest.classes.tap.combinator.js';
|
import { TapCombinator } from './tstest.classes.tap.combinator.js';
|
||||||
@ -14,44 +15,127 @@ export class TestDirectory {
|
|||||||
cwd: string;
|
cwd: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* the relative location of the test dir
|
* the test path or pattern
|
||||||
*/
|
*/
|
||||||
relativePath: string;
|
testPath: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* the absolute path of the test dir
|
* the execution mode
|
||||||
*/
|
*/
|
||||||
absolutePath: string;
|
executionMode: TestExecutionMode;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* an array of Smartfiles
|
* an array of Smartfiles
|
||||||
*/
|
*/
|
||||||
testfileArray: Smartfile[] = [];
|
testfileArray: SmartFile[] = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* the constructor for TestDirectory
|
* the constructor for TestDirectory
|
||||||
* tell it the path
|
* @param cwdArg - the current working directory
|
||||||
* @param pathToTestDirectory
|
* @param testPathArg - the test path/pattern
|
||||||
|
* @param executionModeArg - the execution mode
|
||||||
*/
|
*/
|
||||||
constructor(cwdArg: string, relativePathToTestDirectory: string) {
|
constructor(cwdArg: string, testPathArg: string, executionModeArg: TestExecutionMode) {
|
||||||
this.cwd = cwdArg;
|
this.cwd = cwdArg;
|
||||||
this.relativePath = relativePathToTestDirectory;
|
this.testPath = testPathArg;
|
||||||
|
this.executionMode = executionModeArg;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _init() {
|
private async _init() {
|
||||||
this.testfileArray = await plugins.smartfile.fs.fileTreeToObject(
|
switch (this.executionMode) {
|
||||||
plugins.path.join(this.cwd, this.relativePath),
|
case TestExecutionMode.FILE:
|
||||||
'test*.ts'
|
// Single file mode
|
||||||
);
|
const filePath = plugins.path.isAbsolute(this.testPath)
|
||||||
|
? this.testPath
|
||||||
|
: plugins.path.join(this.cwd, this.testPath);
|
||||||
|
|
||||||
|
if (await plugins.smartfile.fs.fileExists(filePath)) {
|
||||||
|
this.testfileArray = [await plugins.smartfile.SmartFile.fromFilePath(filePath)];
|
||||||
|
} else {
|
||||||
|
throw new Error(`Test file not found: ${filePath}`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TestExecutionMode.GLOB:
|
||||||
|
// Glob pattern mode - use listFileTree which supports glob patterns
|
||||||
|
const globPattern = this.testPath;
|
||||||
|
const matchedFiles = await plugins.smartfile.fs.listFileTree(this.cwd, globPattern);
|
||||||
|
|
||||||
|
this.testfileArray = await Promise.all(
|
||||||
|
matchedFiles.map(async (filePath) => {
|
||||||
|
const absolutePath = plugins.path.isAbsolute(filePath)
|
||||||
|
? filePath
|
||||||
|
: plugins.path.join(this.cwd, filePath);
|
||||||
|
return await plugins.smartfile.SmartFile.fromFilePath(absolutePath);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TestExecutionMode.DIRECTORY:
|
||||||
|
// Directory mode - now recursive with ** pattern
|
||||||
|
const dirPath = plugins.path.join(this.cwd, this.testPath);
|
||||||
|
const testPattern = '**/test*.ts';
|
||||||
|
|
||||||
|
const testFiles = await plugins.smartfile.fs.listFileTree(dirPath, testPattern);
|
||||||
|
|
||||||
|
this.testfileArray = await Promise.all(
|
||||||
|
testFiles.map(async (filePath) => {
|
||||||
|
const absolutePath = plugins.path.isAbsolute(filePath)
|
||||||
|
? filePath
|
||||||
|
: plugins.path.join(dirPath, filePath);
|
||||||
|
return await plugins.smartfile.SmartFile.fromFilePath(absolutePath);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTestFilePathArray() {
|
async getTestFilePathArray() {
|
||||||
await this._init();
|
await this._init();
|
||||||
const testFilePaths: string[] = [];
|
const testFilePaths: string[] = [];
|
||||||
for (const testFile of this.testfileArray) {
|
for (const testFile of this.testfileArray) {
|
||||||
const filePath = plugins.path.join(this.relativePath, testFile.path);
|
// Use the path directly from the SmartFile
|
||||||
testFilePaths.push(filePath);
|
testFilePaths.push(testFile.path);
|
||||||
}
|
}
|
||||||
return testFilePaths;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,9 +7,15 @@ import { coloredString as cs } from '@push.rocks/consolecolor';
|
|||||||
import { TestDirectory } from './tstest.classes.testdirectory.js';
|
import { TestDirectory } from './tstest.classes.testdirectory.js';
|
||||||
import { TapCombinator } from './tstest.classes.tap.combinator.js';
|
import { TapCombinator } from './tstest.classes.tap.combinator.js';
|
||||||
import { TapParser } from './tstest.classes.tap.parser.js';
|
import { TapParser } from './tstest.classes.tap.parser.js';
|
||||||
|
import { TestExecutionMode } from './index.js';
|
||||||
|
import { TsTestLogger } from './tstest.logging.js';
|
||||||
|
import type { LogOptions } from './tstest.logging.js';
|
||||||
|
|
||||||
export class TsTest {
|
export class TsTest {
|
||||||
public testDir: TestDirectory;
|
public testDir: TestDirectory;
|
||||||
|
public executionMode: TestExecutionMode;
|
||||||
|
public logger: TsTestLogger;
|
||||||
|
public filterTags: string[];
|
||||||
|
|
||||||
public smartshellInstance = new plugins.smartshell.Smartshell({
|
public smartshellInstance = new plugins.smartshell.Smartshell({
|
||||||
executor: 'bash',
|
executor: 'bash',
|
||||||
@ -20,61 +26,85 @@ export class TsTest {
|
|||||||
|
|
||||||
public tsbundleInstance = new plugins.tsbundle.TsBundle();
|
public tsbundleInstance = new plugins.tsbundle.TsBundle();
|
||||||
|
|
||||||
constructor(cwdArg: string, relativePathToTestDirectory: string) {
|
constructor(cwdArg: string, testPathArg: string, executionModeArg: TestExecutionMode, logOptions: LogOptions = {}, tags: string[] = []) {
|
||||||
this.testDir = new TestDirectory(cwdArg, relativePathToTestDirectory);
|
this.executionMode = executionModeArg;
|
||||||
|
this.testDir = new TestDirectory(cwdArg, testPathArg, executionModeArg);
|
||||||
|
this.logger = new TsTestLogger(logOptions);
|
||||||
|
this.filterTags = tags;
|
||||||
}
|
}
|
||||||
|
|
||||||
async run() {
|
async run() {
|
||||||
const fileNamesToRun: string[] = await this.testDir.getTestFilePathArray();
|
const testGroups = await this.testDir.getTestFileGroups();
|
||||||
console.log(cs(plugins.figures.hamburger.repeat(80), 'cyan'));
|
const allFiles = [...testGroups.serial, ...Object.values(testGroups.parallelGroups).flat()];
|
||||||
console.log('');
|
|
||||||
console.log(`${logPrefixes.TsTestPrefix} FOUND ${fileNamesToRun.length} TESTFILE(S):`);
|
|
||||||
for (const fileName of fileNamesToRun) {
|
|
||||||
console.log(`${logPrefixes.TsTestPrefix} ${cs(fileName, 'orange')}`);
|
|
||||||
}
|
|
||||||
console.log('-'.repeat(48));
|
|
||||||
console.log(''); // force new line
|
|
||||||
|
|
||||||
const tapCombinator = new TapCombinator(); // lets create the TapCombinator
|
// Log test discovery
|
||||||
for (const fileNameArg of fileNamesToRun) {
|
this.logger.testDiscovery(
|
||||||
switch (true) {
|
allFiles.length,
|
||||||
case process.env.CI && fileNameArg.includes('.nonci.'):
|
this.testDir.testPath,
|
||||||
console.log('!!!!!!!!!!!');
|
this.executionMode
|
||||||
console.log(
|
);
|
||||||
`not running testfile ${fileNameArg}, since we are CI and file name includes '.nonci.' tag`
|
|
||||||
);
|
const tapCombinator = new TapCombinator(this.logger); // lets create the TapCombinator
|
||||||
console.log('!!!!!!!!!!!');
|
let fileIndex = 0;
|
||||||
break;
|
|
||||||
case fileNameArg.endsWith('.browser.ts') || fileNameArg.endsWith('.browser.nonci.ts'):
|
// Execute serial tests first
|
||||||
const tapParserBrowser = await this.runInChrome(fileNameArg);
|
for (const fileNameArg of testGroups.serial) {
|
||||||
tapCombinator.addTapParser(tapParserBrowser);
|
fileIndex++;
|
||||||
break;
|
await this.runSingleTest(fileNameArg, fileIndex, allFiles.length, tapCombinator);
|
||||||
case fileNameArg.endsWith('.both.ts') || fileNameArg.endsWith('.both.nonci.ts'):
|
}
|
||||||
console.log('>>>>>>> TEST PART 1: chrome');
|
|
||||||
const tapParserBothBrowser = await this.runInChrome(fileNameArg);
|
// Execute parallel groups sequentially
|
||||||
tapCombinator.addTapParser(tapParserBothBrowser);
|
const groupNames = Object.keys(testGroups.parallelGroups).sort();
|
||||||
console.log(cs(`|`.repeat(16), 'cyan'));
|
for (const groupName of groupNames) {
|
||||||
console.log(''); // force new line
|
const groupFiles = testGroups.parallelGroups[groupName];
|
||||||
console.log('>>>>>>> TEST PART 2: node');
|
|
||||||
const tapParserBothNode = await this.runInNode(fileNameArg);
|
if (groupFiles.length > 0) {
|
||||||
tapCombinator.addTapParser(tapParserBothNode);
|
this.logger.sectionStart(`Parallel Group: ${groupName}`);
|
||||||
break;
|
|
||||||
default:
|
// Run all tests in this group in parallel
|
||||||
const tapParserNode = await this.runInNode(fileNameArg);
|
const parallelPromises = groupFiles.map(async (fileNameArg) => {
|
||||||
tapCombinator.addTapParser(tapParserNode);
|
fileIndex++;
|
||||||
break;
|
return this.runSingleTest(fileNameArg, fileIndex, allFiles.length, tapCombinator);
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(parallelPromises);
|
||||||
|
this.logger.sectionEnd();
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(cs(`^`.repeat(16), 'cyan'));
|
|
||||||
console.log(''); // force new line
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tapCombinator.evaluate();
|
tapCombinator.evaluate();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async runInNode(fileNameArg: string): Promise<TapParser> {
|
private async runSingleTest(fileNameArg: string, fileIndex: number, totalFiles: number, tapCombinator: TapCombinator) {
|
||||||
console.log(`${cs('=> ', 'blue')} Running ${cs(fileNameArg, 'orange')} in node.js runtime.`);
|
switch (true) {
|
||||||
console.log(`${cs(`= `.repeat(32), 'cyan')}`);
|
case process.env.CI && fileNameArg.includes('.nonci.'):
|
||||||
const tapParser = new TapParser(fileNameArg + ':node');
|
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);
|
||||||
|
const tapParser = new TapParser(fileNameArg + ':node', this.logger);
|
||||||
|
|
||||||
// tsrun options
|
// tsrun options
|
||||||
let tsrunOptions = '';
|
let tsrunOptions = '';
|
||||||
@ -82,6 +112,11 @@ export class TsTest {
|
|||||||
tsrunOptions += ' --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(
|
const execResultStreaming = await this.smartshellInstance.execStreamingSilent(
|
||||||
`tsrun ${fileNameArg}${tsrunOptions}`
|
`tsrun ${fileNameArg}${tsrunOptions}`
|
||||||
);
|
);
|
||||||
@ -89,9 +124,8 @@ export class TsTest {
|
|||||||
return tapParser;
|
return tapParser;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async runInChrome(fileNameArg: string): Promise<TapParser> {
|
public async runInChrome(fileNameArg: string, index: number, total: number): Promise<TapParser> {
|
||||||
console.log(`${cs('=> ', 'blue')} Running ${cs(fileNameArg, 'orange')} in chromium runtime.`);
|
this.logger.testFileStart(fileNameArg, 'chromium', index, total);
|
||||||
console.log(`${cs(`= `.repeat(32), 'cyan')}`);
|
|
||||||
|
|
||||||
// lets get all our paths sorted
|
// lets get all our paths sorted
|
||||||
const tsbundleCacheDirPath = plugins.path.join(paths.cwd, './.nogit/tstest_cache');
|
const tsbundleCacheDirPath = plugins.path.join(paths.cwd, './.nogit/tstest_cache');
|
||||||
@ -129,97 +163,88 @@ export class TsTest {
|
|||||||
server.addRoute('*', new plugins.typedserver.servertools.HandlerStatic(tsbundleCacheDirPath));
|
server.addRoute('*', new plugins.typedserver.servertools.HandlerStatic(tsbundleCacheDirPath));
|
||||||
await server.start();
|
await server.start();
|
||||||
|
|
||||||
|
// lets handle realtime comms
|
||||||
|
const tapParser = new TapParser(fileNameArg + ':chrome', this.logger);
|
||||||
|
const wss = new plugins.ws.WebSocketServer({ port: 8080 });
|
||||||
|
wss.on('connection', (ws) => {
|
||||||
|
ws.on('message', (message) => {
|
||||||
|
const messageStr = message.toString();
|
||||||
|
if (messageStr.startsWith('console:')) {
|
||||||
|
const [, level, ...messageParts] = messageStr.split(':');
|
||||||
|
this.logger.browserConsole(messageParts.join(':'), level);
|
||||||
|
} else {
|
||||||
|
tapParser.handleTapLog(messageStr);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// lets do the browser bit
|
// lets do the browser bit
|
||||||
await this.smartbrowserInstance.start();
|
await this.smartbrowserInstance.start();
|
||||||
const evaluation = await this.smartbrowserInstance.evaluateOnPage(
|
const evaluation = await this.smartbrowserInstance.evaluateOnPage(
|
||||||
`http://localhost:3007/test?bundleName=${bundleFileName}`,
|
`http://localhost:3007/test?bundleName=${bundleFileName}`,
|
||||||
async () => {
|
async () => {
|
||||||
const convertToText = (obj: any): string => {
|
// lets enable real time comms
|
||||||
// create an array that will later be joined into a string.
|
const ws = new WebSocket('ws://localhost:8080');
|
||||||
const stringArray: string[] = [];
|
await new Promise((resolve) => (ws.onopen = resolve));
|
||||||
|
|
||||||
if (typeof obj === 'object' && typeof obj.toString === 'function') {
|
// Ensure this function is declared with 'async'
|
||||||
stringArray.push(obj.toString());
|
const logStore = [];
|
||||||
} else if (typeof obj === 'object' && obj.join === undefined) {
|
const originalLog = console.log;
|
||||||
stringArray.push('{');
|
const originalError = console.error;
|
||||||
for (const prop of Object.keys(obj)) {
|
|
||||||
stringArray.push(prop, ': ', convertToText(obj[prop]), ',');
|
|
||||||
}
|
|
||||||
stringArray.push('}');
|
|
||||||
|
|
||||||
// is array
|
// Override console methods to capture the logs
|
||||||
} else if (typeof obj === 'object' && !(obj.join === undefined)) {
|
|
||||||
stringArray.push('[');
|
|
||||||
for (const prop of Object.keys(obj)) {
|
|
||||||
stringArray.push(convertToText(obj[prop]), ',');
|
|
||||||
}
|
|
||||||
stringArray.push(']');
|
|
||||||
|
|
||||||
// is function
|
|
||||||
} else if (typeof obj === 'function') {
|
|
||||||
stringArray.push(obj.toString());
|
|
||||||
|
|
||||||
// all other values can be done with JSON.stringify
|
|
||||||
} else {
|
|
||||||
stringArray.push(JSON.stringify(obj));
|
|
||||||
}
|
|
||||||
|
|
||||||
return stringArray.join('');
|
|
||||||
};
|
|
||||||
|
|
||||||
let logStore = '';
|
|
||||||
// tslint:disable-next-line: max-classes-per-file
|
|
||||||
const log = console.log.bind(console);
|
|
||||||
console.log = (...args) => {
|
console.log = (...args) => {
|
||||||
args = args.map((argument) => {
|
logStore.push(args.join(' '));
|
||||||
return typeof argument !== 'string' ? convertToText(argument) : argument;
|
ws.send(args.join(' '));
|
||||||
});
|
originalLog(...args);
|
||||||
logStore += `${args}\n`;
|
|
||||||
log(...args);
|
|
||||||
};
|
};
|
||||||
const error = console.error;
|
|
||||||
console.error = (...args) => {
|
console.error = (...args) => {
|
||||||
args = args.map((argument) => {
|
logStore.push(args.join(' '));
|
||||||
return typeof argument !== 'string' ? convertToText(argument) : argument;
|
ws.send(args.join(' '));
|
||||||
});
|
originalError(...args);
|
||||||
logStore += `${args}\n`;
|
|
||||||
error(...args);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const bundleName = new URLSearchParams(window.location.search).get('bundleName');
|
const bundleName = new URLSearchParams(window.location.search).get('bundleName');
|
||||||
console.log(`::TSTEST IN CHROMIUM:: Relevant Script name is: ${bundleName}`);
|
originalLog(`::TSTEST IN CHROMIUM:: Relevant Script name is: ${bundleName}`);
|
||||||
const bundleResponse = await fetch(`/${bundleName}`);
|
|
||||||
console.log(
|
|
||||||
`::TSTEST IN CHROMIUM:: Got ${bundleName} with STATUS ${bundleResponse.status}`
|
|
||||||
);
|
|
||||||
const bundle = await bundleResponse.text();
|
|
||||||
console.log(`::TSTEST IN CHROMIUM:: Executing ${bundleName}`);
|
|
||||||
try {
|
try {
|
||||||
// tslint:disable-next-line: no-eval
|
// Dynamically import the test module
|
||||||
eval(bundle);
|
const testModule = await import(`/${bundleName}`);
|
||||||
|
if (testModule && testModule.default && testModule.default instanceof Promise) {
|
||||||
|
// Execute the exported test function
|
||||||
|
await testModule.default;
|
||||||
|
} else if (testModule && testModule.default && typeof testModule.default.then === 'function') {
|
||||||
|
console.log('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!');
|
||||||
|
console.log('Test module default export is just promiselike: Something might be messing with your Promise implementation.');
|
||||||
|
console.log('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!');
|
||||||
|
await testModule.default;
|
||||||
|
} else if (globalThis.tapPromise && typeof globalThis.tapPromise.then === 'function') {
|
||||||
|
console.log('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!');
|
||||||
|
console.log('Using globalThis.tapPromise');
|
||||||
|
console.log('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!');
|
||||||
|
await testModule.default;
|
||||||
|
} else {
|
||||||
|
console.error('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!');
|
||||||
|
console.error('Test module does not export a default promise.');
|
||||||
|
console.error('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!');
|
||||||
|
console.log(`We got: ${JSON.stringify(testModule)}`);
|
||||||
|
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
return logStore.join('\n');
|
||||||
(globalThis as any).tapbundleDeferred &&
|
|
||||||
(globalThis as any).tapbundleDeferred.promise
|
|
||||||
) {
|
|
||||||
await (globalThis as any).tapbundleDeferred.promise;
|
|
||||||
} else {
|
|
||||||
console.log('Error: Could not find tapbundle Deferred');
|
|
||||||
}
|
|
||||||
return logStore;
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
await this.smartbrowserInstance.stop();
|
await this.smartbrowserInstance.stop();
|
||||||
await server.stop();
|
await server.stop();
|
||||||
|
wss.close();
|
||||||
console.log(
|
console.log(
|
||||||
`${cs('=> ', 'blue')} Stopped ${cs(fileNameArg, 'orange')} chromium instance and server.`
|
`${cs('=> ', 'blue')} Stopped ${cs(fileNameArg, 'orange')} chromium instance and server.`
|
||||||
);
|
);
|
||||||
console.log(`${cs('=> ', 'blue')} See the result captured from the chromium execution:`);
|
|
||||||
// lets create the tap parser
|
// lets create the tap parser
|
||||||
const tapParser = new TapParser(fileNameArg + ':chrome');
|
await tapParser.evaluateFinalResult();
|
||||||
tapParser.handleTapLog(evaluation);
|
|
||||||
return tapParser;
|
return tapParser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
399
ts/tstest.logging.ts
Normal file
399
ts/tstest.logging.ts
Normal file
@ -0,0 +1,399 @@
|
|||||||
|
import { coloredString as cs } from '@push.rocks/consolecolor';
|
||||||
|
import * as plugins from './tstest.plugins.js';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
export interface LogOptions {
|
||||||
|
quiet?: boolean;
|
||||||
|
verbose?: boolean;
|
||||||
|
noColor?: boolean;
|
||||||
|
json?: boolean;
|
||||||
|
logFile?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TestFileResult {
|
||||||
|
file: string;
|
||||||
|
passed: number;
|
||||||
|
failed: number;
|
||||||
|
total: number;
|
||||||
|
duration: number;
|
||||||
|
tests: Array<{
|
||||||
|
name: string;
|
||||||
|
passed: boolean;
|
||||||
|
duration: number;
|
||||||
|
error?: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TestSummary {
|
||||||
|
totalFiles: number;
|
||||||
|
totalTests: number;
|
||||||
|
totalPassed: number;
|
||||||
|
totalFailed: number;
|
||||||
|
totalDuration: number;
|
||||||
|
fileResults: TestFileResult[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TsTestLogger {
|
||||||
|
private options: LogOptions;
|
||||||
|
private startTime: number;
|
||||||
|
private fileResults: TestFileResult[] = [];
|
||||||
|
private currentFileResult: TestFileResult | null = null;
|
||||||
|
private currentTestLogFile: string | null = null;
|
||||||
|
private currentTestLogs: string[] = []; // Buffer for current test logs
|
||||||
|
private currentTestFailed: boolean = false;
|
||||||
|
|
||||||
|
constructor(options: LogOptions = {}) {
|
||||||
|
this.options = options;
|
||||||
|
this.startTime = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
private format(text: string, color?: string): string {
|
||||||
|
if (this.options.noColor || !color) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
return cs(text, color as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
private log(message: string) {
|
||||||
|
if (this.options.json) {
|
||||||
|
// For JSON mode, skip console output
|
||||||
|
// JSON output is handled by logJson method
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(message);
|
||||||
|
|
||||||
|
// Log to the current test file log if we're in a test and --logfile is specified
|
||||||
|
if (this.currentTestLogFile) {
|
||||||
|
this.logToTestFile(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private logToFile(message: string) {
|
||||||
|
// This method is no longer used since we use logToTestFile for individual test logs
|
||||||
|
// Keeping it for potential future use with a global log file
|
||||||
|
}
|
||||||
|
|
||||||
|
private logToTestFile(message: string) {
|
||||||
|
try {
|
||||||
|
// Remove ANSI color codes for file logging
|
||||||
|
const cleanMessage = message.replace(/\u001b\[[0-9;]*m/g, '');
|
||||||
|
|
||||||
|
// Append to test log file
|
||||||
|
fs.appendFileSync(this.currentTestLogFile, cleanMessage + '\n');
|
||||||
|
} catch (error) {
|
||||||
|
// Silently fail to avoid disrupting the test run
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private logJson(data: any) {
|
||||||
|
const jsonString = JSON.stringify(data);
|
||||||
|
console.log(jsonString);
|
||||||
|
|
||||||
|
// Also log to test file if --logfile is specified
|
||||||
|
if (this.currentTestLogFile) {
|
||||||
|
this.logToTestFile(jsonString);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Section separators
|
||||||
|
sectionStart(title: string) {
|
||||||
|
if (this.options.quiet || this.options.json) return;
|
||||||
|
this.log(this.format(`\n━━━ ${title} ━━━`, 'cyan'));
|
||||||
|
}
|
||||||
|
|
||||||
|
sectionEnd() {
|
||||||
|
if (this.options.quiet || this.options.json) return;
|
||||||
|
this.log(this.format('─'.repeat(50), 'dim'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Progress indication
|
||||||
|
progress(current: number, total: number, message: string) {
|
||||||
|
if (this.options.quiet || this.options.json) return;
|
||||||
|
const percentage = Math.round((current / total) * 100);
|
||||||
|
const filled = Math.round((current / total) * 20);
|
||||||
|
const empty = 20 - filled;
|
||||||
|
|
||||||
|
this.log(this.format(`\n📊 Progress: ${current}/${total} (${percentage}%)`, 'cyan'));
|
||||||
|
this.log(this.format(`[${'█'.repeat(filled)}${'░'.repeat(empty)}] ${message}`, 'dim'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test discovery
|
||||||
|
testDiscovery(count: number, pattern: string, executionMode: string) {
|
||||||
|
if (this.options.json) {
|
||||||
|
this.logJson({ event: 'discovery', count, pattern, executionMode });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.options.quiet) {
|
||||||
|
this.log(`Found ${count} tests`);
|
||||||
|
} else {
|
||||||
|
this.log(this.format(`\n🔍 Test Discovery`, 'bold'));
|
||||||
|
this.log(this.format(` Mode: ${executionMode}`, 'dim'));
|
||||||
|
this.log(this.format(` Pattern: ${pattern}`, 'dim'));
|
||||||
|
this.log(this.format(` Found: ${count} test file(s)`, 'green'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test execution
|
||||||
|
testFileStart(filename: string, runtime: string, index: number, total: number) {
|
||||||
|
this.currentFileResult = {
|
||||||
|
file: filename,
|
||||||
|
passed: 0,
|
||||||
|
failed: 0,
|
||||||
|
total: 0,
|
||||||
|
duration: 0,
|
||||||
|
tests: []
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reset test-specific state
|
||||||
|
this.currentTestLogs = [];
|
||||||
|
this.currentTestFailed = false;
|
||||||
|
|
||||||
|
// Only set up test log file if --logfile option is specified
|
||||||
|
if (this.options.logFile) {
|
||||||
|
const baseFilename = path.basename(filename, '.ts');
|
||||||
|
this.currentTestLogFile = path.join('.nogit', 'testlogs', `${baseFilename}.log`);
|
||||||
|
|
||||||
|
// Ensure the directory exists
|
||||||
|
const logDir = path.dirname(this.currentTestLogFile);
|
||||||
|
if (!fs.existsSync(logDir)) {
|
||||||
|
fs.mkdirSync(logDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the log file for this test
|
||||||
|
fs.writeFileSync(this.currentTestLogFile, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.options.json) {
|
||||||
|
this.logJson({ event: 'fileStart', filename, runtime, index, total });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.options.quiet) return;
|
||||||
|
|
||||||
|
this.log(this.format(`\n▶️ ${filename} (${index}/${total})`, 'blue'));
|
||||||
|
this.log(this.format(` Runtime: ${runtime}`, 'dim'));
|
||||||
|
}
|
||||||
|
|
||||||
|
testResult(testName: string, passed: boolean, duration: number, error?: string) {
|
||||||
|
if (this.currentFileResult) {
|
||||||
|
this.currentFileResult.tests.push({ name: testName, passed, duration, error });
|
||||||
|
this.currentFileResult.total++;
|
||||||
|
if (passed) {
|
||||||
|
this.currentFileResult.passed++;
|
||||||
|
} else {
|
||||||
|
this.currentFileResult.failed++;
|
||||||
|
this.currentTestFailed = true;
|
||||||
|
}
|
||||||
|
this.currentFileResult.duration += duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.options.json) {
|
||||||
|
this.logJson({ event: 'testResult', testName, passed, duration, error });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If test failed and we have buffered logs, show them now
|
||||||
|
if (!passed && this.currentTestLogs.length > 0 && !this.options.verbose) {
|
||||||
|
this.log(this.format(' 📋 Console output from failed test:', 'yellow'));
|
||||||
|
this.currentTestLogs.forEach(logMessage => {
|
||||||
|
this.log(this.format(` ${logMessage}`, 'dim'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const icon = passed ? '✅' : '❌';
|
||||||
|
const color = passed ? 'green' : 'red';
|
||||||
|
|
||||||
|
if (this.options.quiet) {
|
||||||
|
this.log(`${icon} ${testName}`);
|
||||||
|
} else {
|
||||||
|
this.log(this.format(` ${icon} ${testName} (${duration}ms)`, color));
|
||||||
|
if (error && !passed) {
|
||||||
|
this.log(this.format(` ${error}`, 'red'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear logs after each test
|
||||||
|
this.currentTestLogs = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
testFileEnd(passed: number, failed: number, duration: number) {
|
||||||
|
if (this.currentFileResult) {
|
||||||
|
this.fileResults.push(this.currentFileResult);
|
||||||
|
this.currentFileResult = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.options.json) {
|
||||||
|
this.logJson({ event: 'fileEnd', passed, failed, duration });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.options.quiet) {
|
||||||
|
const total = passed + failed;
|
||||||
|
const status = failed === 0 ? 'PASSED' : 'FAILED';
|
||||||
|
const color = failed === 0 ? 'green' : 'red';
|
||||||
|
this.log(this.format(` Summary: ${passed}/${total} ${status}`, color));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the current test log file reference only if using --logfile
|
||||||
|
if (this.options.logFile) {
|
||||||
|
this.currentTestLogFile = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TAP output forwarding (for TAP protocol messages)
|
||||||
|
tapOutput(message: string, isError: boolean = false) {
|
||||||
|
if (this.options.json) return;
|
||||||
|
|
||||||
|
// Never show raw TAP protocol messages in console
|
||||||
|
// They are already processed by TapParser and shown in our format
|
||||||
|
|
||||||
|
// Always log to test file if --logfile is specified
|
||||||
|
if (this.currentTestLogFile) {
|
||||||
|
this.logToTestFile(` ${message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Console output from test files (non-TAP output)
|
||||||
|
testConsoleOutput(message: string) {
|
||||||
|
if (this.options.json) return;
|
||||||
|
|
||||||
|
// In verbose mode, show console output immediately
|
||||||
|
if (this.options.verbose) {
|
||||||
|
this.log(this.format(` ${message}`, 'dim'));
|
||||||
|
} else {
|
||||||
|
// In non-verbose mode, buffer the logs
|
||||||
|
this.currentTestLogs.push(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always log to test file if --logfile is specified
|
||||||
|
if (this.currentTestLogFile) {
|
||||||
|
this.logToTestFile(` ${message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Browser console
|
||||||
|
browserConsole(message: string, level: string = 'log') {
|
||||||
|
if (this.options.json) {
|
||||||
|
this.logJson({ event: 'browserConsole', message, level });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.options.quiet) {
|
||||||
|
const prefix = level === 'error' ? '🌐❌' : '🌐';
|
||||||
|
const color = level === 'error' ? 'red' : 'magenta';
|
||||||
|
this.log(this.format(` ${prefix} ${message}`, color));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test error details display
|
||||||
|
testErrorDetails(errorMessage: string) {
|
||||||
|
if (this.options.json) {
|
||||||
|
this.logJson({ event: 'testError', error: errorMessage });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.options.quiet) {
|
||||||
|
this.log(this.format(' Error details:', 'red'));
|
||||||
|
errorMessage.split('\n').forEach(line => {
|
||||||
|
this.log(this.format(` ${line}`, 'red'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always log to test file if --logfile is specified
|
||||||
|
if (this.currentTestLogFile) {
|
||||||
|
this.logToTestFile(` Error: ${errorMessage}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final summary
|
||||||
|
summary() {
|
||||||
|
const totalDuration = Date.now() - this.startTime;
|
||||||
|
const summary: TestSummary = {
|
||||||
|
totalFiles: this.fileResults.length,
|
||||||
|
totalTests: this.fileResults.reduce((sum, r) => sum + r.total, 0),
|
||||||
|
totalPassed: this.fileResults.reduce((sum, r) => sum + r.passed, 0),
|
||||||
|
totalFailed: this.fileResults.reduce((sum, r) => sum + r.failed, 0),
|
||||||
|
totalDuration,
|
||||||
|
fileResults: this.fileResults
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.options.json) {
|
||||||
|
this.logJson({ event: 'summary', summary });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.options.quiet) {
|
||||||
|
const status = summary.totalFailed === 0 ? 'PASSED' : 'FAILED';
|
||||||
|
this.log(`\nSummary: ${summary.totalPassed}/${summary.totalTests} | ${totalDuration}ms | ${status}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detailed summary
|
||||||
|
this.log(this.format('\n📊 Test Summary', 'bold'));
|
||||||
|
this.log(this.format('┌────────────────────────────────┐', 'dim'));
|
||||||
|
this.log(this.format(`│ Total Files: ${summary.totalFiles.toString().padStart(14)} │`, 'white'));
|
||||||
|
this.log(this.format(`│ Total Tests: ${summary.totalTests.toString().padStart(14)} │`, 'white'));
|
||||||
|
this.log(this.format(`│ Passed: ${summary.totalPassed.toString().padStart(14)} │`, 'green'));
|
||||||
|
this.log(this.format(`│ Failed: ${summary.totalFailed.toString().padStart(14)} │`, summary.totalFailed > 0 ? 'red' : 'green'));
|
||||||
|
this.log(this.format(`│ Duration: ${totalDuration.toString().padStart(14)}ms │`, 'white'));
|
||||||
|
this.log(this.format('└────────────────────────────────┘', 'dim'));
|
||||||
|
|
||||||
|
// File results
|
||||||
|
if (summary.totalFailed > 0) {
|
||||||
|
this.log(this.format('\n❌ Failed Tests:', 'red'));
|
||||||
|
this.fileResults.forEach(fileResult => {
|
||||||
|
if (fileResult.failed > 0) {
|
||||||
|
this.log(this.format(`\n ${fileResult.file}`, 'yellow'));
|
||||||
|
fileResult.tests.filter(t => !t.passed).forEach(test => {
|
||||||
|
this.log(this.format(` ❌ ${test.name}`, 'red'));
|
||||||
|
if (test.error) {
|
||||||
|
this.log(this.format(` ${test.error}`, 'dim'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Performance metrics
|
||||||
|
if (this.options.verbose) {
|
||||||
|
const avgDuration = Math.round(totalDuration / summary.totalTests);
|
||||||
|
const slowestTest = this.fileResults
|
||||||
|
.flatMap(r => r.tests)
|
||||||
|
.sort((a, b) => b.duration - a.duration)[0];
|
||||||
|
|
||||||
|
this.log(this.format('\n⏱️ Performance Metrics:', 'cyan'));
|
||||||
|
this.log(this.format(` Average per test: ${avgDuration}ms`, 'white'));
|
||||||
|
if (slowestTest) {
|
||||||
|
this.log(this.format(` Slowest test: ${slowestTest.name} (${slowestTest.duration}ms)`, 'yellow'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final status
|
||||||
|
const status = summary.totalFailed === 0 ? 'ALL TESTS PASSED! 🎉' : 'SOME TESTS FAILED! ❌';
|
||||||
|
const statusColor = summary.totalFailed === 0 ? 'green' : 'red';
|
||||||
|
this.log(this.format(`\n${status}`, statusColor));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error display
|
||||||
|
error(message: string, file?: string, stack?: string) {
|
||||||
|
if (this.options.json) {
|
||||||
|
this.logJson({ event: 'error', message, file, stack });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.options.quiet) {
|
||||||
|
console.error(`ERROR: ${message}`);
|
||||||
|
} else {
|
||||||
|
this.log(this.format('\n⚠️ Error', 'red'));
|
||||||
|
if (file) this.log(this.format(` File: ${file}`, 'yellow'));
|
||||||
|
this.log(this.format(` ${message}`, 'red'));
|
||||||
|
if (stack && this.options.verbose) {
|
||||||
|
this.log(this.format(` Stack:`, 'dim'));
|
||||||
|
this.log(this.format(stack.split('\n').map(line => ` ${line}`).join('\n'), 'dim'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@ import * as plugins from './tstest.plugins.js';
|
|||||||
import { coloredString as cs } from '@push.rocks/consolecolor';
|
import { coloredString as cs } from '@push.rocks/consolecolor';
|
||||||
|
|
||||||
export const TapPrefix = cs(`::TAP::`, 'pink', 'black');
|
export const TapPrefix = cs(`::TAP::`, 'pink', 'black');
|
||||||
|
export const TapPretaskPrefix = cs(`::PRETASK::`, 'cyan', 'black');
|
||||||
export const TapErrorPrefix = cs(` !!!TAP PROTOCOL ERROR!!! `, 'red', 'black');
|
export const TapErrorPrefix = cs(` !!!TAP PROTOCOL ERROR!!! `, 'red', 'black');
|
||||||
|
|
||||||
export const TsTestPrefix = cs(`**TSTEST**`, 'pink', 'black');
|
export const TsTestPrefix = cs(`**TSTEST**`, 'pink', 'black');
|
||||||
|
@ -4,7 +4,7 @@ import * as path from 'path';
|
|||||||
export { path };
|
export { path };
|
||||||
|
|
||||||
// @apiglobal scope
|
// @apiglobal scope
|
||||||
import * as typedserver from '@apiglobal/typedserver';
|
import * as typedserver from '@api.global/typedserver';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
typedserver
|
typedserver
|
||||||
@ -18,7 +18,7 @@ import * as smartfile from '@push.rocks/smartfile';
|
|||||||
import * as smartlog from '@push.rocks/smartlog';
|
import * as smartlog from '@push.rocks/smartlog';
|
||||||
import * as smartpromise from '@push.rocks/smartpromise';
|
import * as smartpromise from '@push.rocks/smartpromise';
|
||||||
import * as smartshell from '@push.rocks/smartshell';
|
import * as smartshell from '@push.rocks/smartshell';
|
||||||
import * as tapbundle from '@push.rocks/tapbundle';
|
import * as tapbundle from '../dist_ts_tapbundle/index.js';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
consolecolor,
|
consolecolor,
|
||||||
@ -40,3 +40,10 @@ export { tsbundle };
|
|||||||
import figures from 'figures';
|
import figures from 'figures';
|
||||||
|
|
||||||
export { figures };
|
export { figures };
|
||||||
|
|
||||||
|
// third party
|
||||||
|
import * as ws from 'ws';
|
||||||
|
|
||||||
|
export {
|
||||||
|
ws
|
||||||
|
}
|
||||||
|
8
ts_tapbundle/00_commitinfo_data.ts
Normal file
8
ts_tapbundle/00_commitinfo_data.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* autocreated commitinfo by @push.rocks/commitinfo
|
||||||
|
*/
|
||||||
|
export const commitinfo = {
|
||||||
|
name: '@push.rocks/tapbundle',
|
||||||
|
version: '6.0.3',
|
||||||
|
description: 'A comprehensive testing automation library that provides a wide range of utilities and tools for TAP (Test Anything Protocol) based testing, especially suitable for projects using tapbuffer.'
|
||||||
|
}
|
8
ts_tapbundle/index.ts
Normal file
8
ts_tapbundle/index.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export { tap } from './tapbundle.classes.tap.js';
|
||||||
|
export { TapWrap } from './tapbundle.classes.tapwrap.js';
|
||||||
|
export { webhelpers } from './webhelpers.js';
|
||||||
|
export { TapTools } from './tapbundle.classes.taptools.js';
|
||||||
|
|
||||||
|
import { expect } from '@push.rocks/smartexpect';
|
||||||
|
|
||||||
|
export { expect };
|
21
ts_tapbundle/tapbundle.classes.pretask.ts
Normal file
21
ts_tapbundle/tapbundle.classes.pretask.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import * as plugins from './tapbundle.plugins.js';
|
||||||
|
import { TapTools } from './tapbundle.classes.taptools.js';
|
||||||
|
|
||||||
|
export interface IPreTaskFunction {
|
||||||
|
(tapTools?: TapTools): Promise<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PreTask {
|
||||||
|
public description: string;
|
||||||
|
public preTaskFunction: IPreTaskFunction;
|
||||||
|
|
||||||
|
constructor(descriptionArg: string, preTaskFunctionArg: IPreTaskFunction) {
|
||||||
|
this.description = descriptionArg;
|
||||||
|
this.preTaskFunction = preTaskFunctionArg;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async run() {
|
||||||
|
console.log(`::__PRETASK: ${this.description}`);
|
||||||
|
await this.preTaskFunction(new TapTools(null));
|
||||||
|
}
|
||||||
|
}
|
497
ts_tapbundle/tapbundle.classes.tap.ts
Normal file
497
ts_tapbundle/tapbundle.classes.tap.ts
Normal file
@ -0,0 +1,497 @@
|
|||||||
|
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
|
||||||
|
*/
|
||||||
|
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++;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* only executes tests marked as ONLY
|
||||||
|
*/
|
||||||
|
public only = {
|
||||||
|
test: (descriptionArg: string, testFunctionArg: ITestFunction<T>) => {
|
||||||
|
this.test(descriptionArg, testFunctionArg, 'only');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
private _tapPreTasks: PreTask[] = [];
|
||||||
|
private _tapTests: TapTest<any>[] = [];
|
||||||
|
private _tapTestsOnly: TapTest<any>[] = [];
|
||||||
|
private _currentSuite: ITestSuite | null = null;
|
||||||
|
private _rootSuites: ITestSuite[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normal test function, will run one by one
|
||||||
|
* @param testDescription - A description of what the test does
|
||||||
|
* @param testFunction - A Function that returns a Promise and resolves or rejects
|
||||||
|
*/
|
||||||
|
public test(
|
||||||
|
testDescription: string,
|
||||||
|
testFunction: ITestFunction<T>,
|
||||||
|
modeArg: 'normal' | 'only' | 'skip' = 'normal'
|
||||||
|
): TapTest<T> {
|
||||||
|
const localTest = new TapTest<T>({
|
||||||
|
description: testDescription,
|
||||||
|
testFunction,
|
||||||
|
parallel: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public preTask(descriptionArg: string, functionArg: IPreTaskFunction) {
|
||||||
|
this._tapPreTasks.push(new PreTask(descriptionArg, functionArg));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A parallel test that will not be waited for before the next starts.
|
||||||
|
* @param testDescription - A description of what the test does
|
||||||
|
* @param testFunction - A Function that returns a Promise and resolves or rejects
|
||||||
|
*/
|
||||||
|
public testParallel(testDescription: string, testFunction: ITestFunction<T>) {
|
||||||
|
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 = 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 (allTests.length === 0 && this._tapTestsOnly.length === 0) {
|
||||||
|
console.log('no tests specified. Ending here!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// determine which tests to run
|
||||||
|
let concerningTests: TapTest[];
|
||||||
|
if (this._tapTestsOnly.length > 0) {
|
||||||
|
concerningTests = this._tapTestsOnly;
|
||||||
|
} else {
|
||||||
|
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
|
||||||
|
for (const preTask of this._tapPreTasks) {
|
||||||
|
await preTask.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count actual tests that will be run
|
||||||
|
console.log(`1..${concerningTests.length}`);
|
||||||
|
|
||||||
|
// 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
|
||||||
|
const failReasons: string[] = [];
|
||||||
|
const executionNotes: string[] = [];
|
||||||
|
// collect failed tests
|
||||||
|
for (const tapTest of concerningTests) {
|
||||||
|
if (tapTest.status !== 'success' && tapTest.status !== 'skipped') {
|
||||||
|
failReasons.push(
|
||||||
|
`Test ${tapTest.testKey + 1} failed with status ${tapTest.status}:\n` +
|
||||||
|
`|| ${tapTest.description}\n` +
|
||||||
|
`|| for more information please take a look the logs above`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// render fail Reasons
|
||||||
|
for (const failReason of failReasons) {
|
||||||
|
console.log(failReason);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (optionsArg && optionsArg.throwOnError && failReasons.length > 0) {
|
||||||
|
if (!smartenvInstance.isBrowser && typeof process !== 'undefined') process.exit(1);
|
||||||
|
}
|
||||||
|
if (smartenvInstance.isBrowser) {
|
||||||
|
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 (typeof process !== 'undefined') {
|
||||||
|
if (directArg) {
|
||||||
|
process.exit(codeArg);
|
||||||
|
} else {
|
||||||
|
setTimeout(() => {
|
||||||
|
process.exit(codeArg);
|
||||||
|
}, 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* handle errors
|
||||||
|
*/
|
||||||
|
public threw(err: Error) {
|
||||||
|
console.log(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Explicitly fail the current test with a custom message
|
||||||
|
* @param message - The failure message to display
|
||||||
|
*/
|
||||||
|
public fail(message: string = 'Test failed'): never {
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const tap = new Tap();
|
153
ts_tapbundle/tapbundle.classes.taptest.ts
Normal file
153
ts_tapbundle/tapbundle.classes.taptest.ts
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
import * as plugins from './tapbundle.plugins.js';
|
||||||
|
import { tapCreator } from './tapbundle.tapcreator.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' | 'skipped';
|
||||||
|
|
||||||
|
export interface ITestFunction<T> {
|
||||||
|
(tapTools?: TapTools): Promise<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TapTest<T = unknown> {
|
||||||
|
public description: string;
|
||||||
|
public failureAllowed: boolean;
|
||||||
|
public hrtMeasurement: HrtMeasurement;
|
||||||
|
public parallel: boolean;
|
||||||
|
public status: TTestStatus;
|
||||||
|
public tapTools: TapTools;
|
||||||
|
public testFunction: ITestFunction<T>;
|
||||||
|
public testKey: number; // the testKey the position in the test qeue. Set upon calling .run()
|
||||||
|
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();
|
||||||
|
public testResultPromise: Promise<T> = this.testResultDeferred.promise;
|
||||||
|
/**
|
||||||
|
* constructor
|
||||||
|
*/
|
||||||
|
constructor(optionsArg: {
|
||||||
|
description: string;
|
||||||
|
testFunction: ITestFunction<T>;
|
||||||
|
parallel: boolean;
|
||||||
|
}) {
|
||||||
|
this.description = optionsArg.description;
|
||||||
|
this.hrtMeasurement = new HrtMeasurement();
|
||||||
|
this.parallel = optionsArg.parallel;
|
||||||
|
this.status = 'pending';
|
||||||
|
this.tapTools = new TapTools(this);
|
||||||
|
this.testFunction = optionsArg.testFunction;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* run the test
|
||||||
|
*/
|
||||||
|
public async run(testKeyArg: number) {
|
||||||
|
this.testKey = testKeyArg;
|
||||||
|
const testNumber = testKeyArg + 1;
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
271
ts_tapbundle/tapbundle.classes.taptools.ts
Normal file
271
ts_tapbundle/tapbundle.classes.taptools.ts
Normal file
@ -0,0 +1,271 @@
|
|||||||
|
import * as plugins from './tapbundle.plugins.js';
|
||||||
|
import { TapTest } from './tapbundle.classes.taptest.js';
|
||||||
|
|
||||||
|
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`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* allow failure
|
||||||
|
*/
|
||||||
|
public allowFailure() {
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
public async delayFor(timeMilliArg: number) {
|
||||||
|
await plugins.smartdelay.delayFor(timeMilliArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async delayForRandom(timeMilliMinArg: number, timeMilliMaxArg: number) {
|
||||||
|
await plugins.smartdelay.delayForRandom(timeMilliMinArg, timeMilliMaxArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async coloredString(...args: Parameters<typeof plugins.consolecolor.coloredString>) {
|
||||||
|
return plugins.consolecolor.coloredString(...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
if (this._tapTest.status === 'pending') {
|
||||||
|
this._tapTest.status = 'timeout';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async returnError(throwingFuncArg: IPromiseFunc) {
|
||||||
|
let funcErr: Error;
|
||||||
|
try {
|
||||||
|
await throwingFuncArg();
|
||||||
|
} catch (err: any) {
|
||||||
|
funcErr = err;
|
||||||
|
}
|
||||||
|
return funcErr;
|
||||||
|
}
|
||||||
|
|
||||||
|
public defer() {
|
||||||
|
return plugins.smartpromise.defer();
|
||||||
|
}
|
||||||
|
|
||||||
|
public cumulativeDefer() {
|
||||||
|
return plugins.smartpromise.cumulativeDefer();
|
||||||
|
}
|
||||||
|
|
||||||
|
public smartjson = plugins.smartjson;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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();
|
||||||
|
}
|
||||||
|
}
|
13
ts_tapbundle/tapbundle.classes.tapwrap.ts
Normal file
13
ts_tapbundle/tapbundle.classes.tapwrap.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import * as plugins from './tapbundle.plugins.js';
|
||||||
|
|
||||||
|
export interface ITapWrapOptions {
|
||||||
|
before: () => Promise<any>;
|
||||||
|
after: () => {};
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TapWrap {
|
||||||
|
public options: ITapWrapOptions;
|
||||||
|
constructor(optionsArg: ITapWrapOptions) {
|
||||||
|
this.options = optionsArg;
|
||||||
|
}
|
||||||
|
}
|
9
ts_tapbundle/tapbundle.plugins.ts
Normal file
9
ts_tapbundle/tapbundle.plugins.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
// pushrocks
|
||||||
|
import * as consolecolor from '@push.rocks/consolecolor';
|
||||||
|
import * as smartdelay from '@push.rocks/smartdelay';
|
||||||
|
import * as smartenv from '@push.rocks/smartenv';
|
||||||
|
import * as smartexpect from '@push.rocks/smartexpect';
|
||||||
|
import * as smartjson from '@push.rocks/smartjson';
|
||||||
|
import * as smartpromise from '@push.rocks/smartpromise';
|
||||||
|
|
||||||
|
export { consolecolor, smartdelay, smartenv, smartexpect, smartjson, smartpromise };
|
7
ts_tapbundle/tapbundle.tapcreator.ts
Normal file
7
ts_tapbundle/tapbundle.tapcreator.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import * as plugins from './tapbundle.plugins.js';
|
||||||
|
|
||||||
|
export class TapCreator {
|
||||||
|
// TODO:
|
||||||
|
}
|
||||||
|
|
||||||
|
export let tapCreator = new TapCreator();
|
3
ts_tapbundle/tspublish.json
Normal file
3
ts_tapbundle/tspublish.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"order": 1
|
||||||
|
}
|
40
ts_tapbundle/webhelpers.ts
Normal file
40
ts_tapbundle/webhelpers.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import * as plugins from './tapbundle.plugins.js';
|
||||||
|
import { tap } from './tapbundle.classes.tap.js';
|
||||||
|
|
||||||
|
class WebHelpers {
|
||||||
|
html: any;
|
||||||
|
fixture: any;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
const smartenv = new plugins.smartenv.Smartenv();
|
||||||
|
|
||||||
|
// Initialize HTML template tag function
|
||||||
|
this.html = (strings: TemplateStringsArray, ...values: any[]) => {
|
||||||
|
let result = '';
|
||||||
|
for (let i = 0; i < strings.length; i++) {
|
||||||
|
result += strings[i];
|
||||||
|
if (i < values.length) {
|
||||||
|
result += values[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize fixture function based on environment
|
||||||
|
if (smartenv.isBrowser) {
|
||||||
|
this.fixture = async (htmlString: string): Promise<HTMLElement> => {
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.innerHTML = htmlString.trim();
|
||||||
|
const element = container.firstChild as HTMLElement;
|
||||||
|
return element;
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Node.js environment - provide a stub or alternative implementation
|
||||||
|
this.fixture = async (htmlString: string): Promise<any> => {
|
||||||
|
throw new Error('WebHelpers.fixture is only available in browser environment');
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const webhelpers = new WebHelpers();
|
98
ts_tapbundle_node/classes.tapnodetools.ts
Normal file
98
ts_tapbundle_node/classes.tapnodetools.ts
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import { TestFileProvider } from './classes.testfileprovider.js';
|
||||||
|
import * as plugins from './plugins.js';
|
||||||
|
|
||||||
|
class TapNodeTools {
|
||||||
|
private smartshellInstance: plugins.smartshell.Smartshell;
|
||||||
|
public testFileProvider = new TestFileProvider();
|
||||||
|
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
private qenv: plugins.qenv.Qenv;
|
||||||
|
public async getQenv(): Promise<plugins.qenv.Qenv> {
|
||||||
|
this.qenv = this.qenv || new plugins.qenv.Qenv('./', '.nogit/');
|
||||||
|
return this.qenv;
|
||||||
|
}
|
||||||
|
public async getEnvVarOnDemand(envVarNameArg: string): Promise<string> {
|
||||||
|
const qenv = await this.getQenv();
|
||||||
|
return qenv.getEnvVarOnDemand(envVarNameArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async runCommand(commandArg: string): Promise<any> {
|
||||||
|
if (!this.smartshellInstance) {
|
||||||
|
this.smartshellInstance = new plugins.smartshell.Smartshell({
|
||||||
|
executor: 'bash',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const result = await this.smartshellInstance.exec(commandArg);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async createHttpsCert(
|
||||||
|
commonName: string = 'localhost',
|
||||||
|
allowSelfSigned: boolean = true
|
||||||
|
): Promise<{ key: string; cert: string }> {
|
||||||
|
if (allowSelfSigned) {
|
||||||
|
// set node to allow self-signed certificates
|
||||||
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a key pair
|
||||||
|
const keys = plugins.smartcrypto.nodeForge.pki.rsa.generateKeyPair(2048);
|
||||||
|
|
||||||
|
// Create a self-signed certificate
|
||||||
|
const cert = plugins.smartcrypto.nodeForge.pki.createCertificate();
|
||||||
|
cert.publicKey = keys.publicKey;
|
||||||
|
cert.serialNumber = '01';
|
||||||
|
cert.validity.notBefore = new Date();
|
||||||
|
cert.validity.notAfter = new Date();
|
||||||
|
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1);
|
||||||
|
|
||||||
|
const attrs = [
|
||||||
|
{ name: 'commonName', value: commonName },
|
||||||
|
{ name: 'countryName', value: 'US' },
|
||||||
|
{ shortName: 'ST', value: 'California' },
|
||||||
|
{ name: 'localityName', value: 'San Francisco' },
|
||||||
|
{ name: 'organizationName', value: 'My Company' },
|
||||||
|
{ shortName: 'OU', value: 'Dev' },
|
||||||
|
];
|
||||||
|
cert.setSubject(attrs);
|
||||||
|
cert.setIssuer(attrs);
|
||||||
|
|
||||||
|
// Sign the certificate with its own private key (self-signed)
|
||||||
|
cert.sign(keys.privateKey, plugins.smartcrypto.nodeForge.md.sha256.create());
|
||||||
|
|
||||||
|
// PEM encode the private key and certificate
|
||||||
|
const pemKey = plugins.smartcrypto.nodeForge.pki.privateKeyToPem(keys.privateKey);
|
||||||
|
const pemCert = plugins.smartcrypto.nodeForge.pki.certificateToPem(cert);
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: pemKey,
|
||||||
|
cert: pemCert,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* create and return a smartmongo instance
|
||||||
|
*/
|
||||||
|
public async createSmartmongo() {
|
||||||
|
const smartmongoMod = await import('@push.rocks/smartmongo');
|
||||||
|
const smartmongoInstance = new smartmongoMod.SmartMongo();
|
||||||
|
await smartmongoInstance.start();
|
||||||
|
return smartmongoInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* create and return a smarts3 instance
|
||||||
|
*/
|
||||||
|
public async createSmarts3() {
|
||||||
|
const smarts3Mod = await import('@push.rocks/smarts3');
|
||||||
|
const smarts3Instance = new smarts3Mod.Smarts3({
|
||||||
|
port: 3003,
|
||||||
|
cleanSlate: true,
|
||||||
|
});
|
||||||
|
await smarts3Instance.start();
|
||||||
|
return smarts3Instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const tapNodeTools = new TapNodeTools();
|
17
ts_tapbundle_node/classes.testfileprovider.ts
Normal file
17
ts_tapbundle_node/classes.testfileprovider.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import * as plugins from './plugins.js';
|
||||||
|
import * as paths from './paths.js';
|
||||||
|
|
||||||
|
export const fileUrls = {
|
||||||
|
dockerAlpineImage: 'https://code.foss.global/testassets/docker/raw/branch/main/alpine.tar',
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TestFileProvider {
|
||||||
|
public async getDockerAlpineImageAsLocalTarball(): Promise<string> {
|
||||||
|
const filePath = plugins.path.join(paths.testFilesDir, 'alpine.tar')
|
||||||
|
// fetch the docker alpine image
|
||||||
|
const response = await plugins.smartrequest.getBinary(fileUrls.dockerAlpineImage);
|
||||||
|
await plugins.smartfile.fs.ensureDir(paths.testFilesDir);
|
||||||
|
await plugins.smartfile.memory.toFs(response.body, filePath);
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
}
|
2
ts_tapbundle_node/index.ts
Normal file
2
ts_tapbundle_node/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './classes.tapnodetools.js';
|
||||||
|
|
4
ts_tapbundle_node/paths.ts
Normal file
4
ts_tapbundle_node/paths.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import * as plugins from './plugins.js';
|
||||||
|
|
||||||
|
export const cwd = process.cwd();
|
||||||
|
export const testFilesDir = plugins.path.join(cwd, './.nogit/testfiles/');
|
16
ts_tapbundle_node/plugins.ts
Normal file
16
ts_tapbundle_node/plugins.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
// node native
|
||||||
|
import * as crypto from 'crypto';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
export { crypto,fs, path, };
|
||||||
|
|
||||||
|
// @push.rocks scope
|
||||||
|
import * as qenv from '@push.rocks/qenv';
|
||||||
|
import * as smartcrypto from '@push.rocks/smartcrypto';
|
||||||
|
import * as smartfile from '@push.rocks/smartfile';
|
||||||
|
import * as smartpath from '@push.rocks/smartpath';
|
||||||
|
import * as smartrequest from '@push.rocks/smartrequest';
|
||||||
|
import * as smartshell from '@push.rocks/smartshell';
|
||||||
|
|
||||||
|
export { qenv, smartcrypto, smartfile, smartpath, smartrequest, smartshell, };
|
@ -3,9 +3,12 @@
|
|||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"useDefineForClassFields": false,
|
"useDefineForClassFields": false,
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"module": "ES2022",
|
"module": "NodeNext",
|
||||||
"moduleResolution": "nodenext",
|
"moduleResolution": "NodeNext",
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"verbatimModuleSyntax": true,
|
"verbatimModuleSyntax": true
|
||||||
}
|
},
|
||||||
|
"exclude": [
|
||||||
|
"dist_*/**/*.d.ts"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user