feat(logging): Improve logging output, CLI option parsing, and test report formatting.
This commit is contained in:
		| @@ -1,5 +1,14 @@ | ||||
| # Changelog | ||||
|  | ||||
| ## 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 | ||||
|  | ||||
|   | ||||
							
								
								
									
										230
									
								
								readme.plan.md
									
									
									
									
									
								
							
							
						
						
									
										230
									
								
								readme.plan.md
									
									
									
									
									
								
							| @@ -1,51 +1,199 @@ | ||||
| # Plan for adding single file and glob pattern execution support to tstest | ||||
| # Plan for improving logging and output in tstest | ||||
|  | ||||
| !! FIRST: Reread /home/philkunz/.claude/CLAUDE.md to ensure following all guidelines !! | ||||
|  | ||||
| ## Goal - ✅ COMPLETED | ||||
| - ✅ Make `tstest test/test.abc.ts` run the specified file directly | ||||
| - ✅ Support glob patterns like `tstest test/*.spec.ts` or `tstest test/**/*.test.ts` | ||||
| - ✅ Maintain backward compatibility with directory argument | ||||
| ## Goal - ✅ MOSTLY COMPLETED | ||||
| - ✅ Make test output cleaner and more visually appealing | ||||
| - ✅ Add structured logging capabilities | ||||
| - ✅ Support different verbosity levels | ||||
| - ✅ Improve CI/CD compatibility | ||||
| - ✅ Add progress indicators and timing summaries | ||||
|  | ||||
| ## Current behavior - UPDATED | ||||
| - ✅ tstest now supports three modes: directory, single file, and glob patterns | ||||
| - ✅ Directory mode now searches recursively using `**/test*.ts` pattern | ||||
| - ✅ Single file mode runs a specific test file | ||||
| - ✅ Glob mode runs files matching the pattern | ||||
| ## Current State - UPDATED | ||||
| - ✅ Clean, modern visual design with Unicode characters | ||||
| - ✅ Structured output format (JSON support) | ||||
| - ✅ Multiple verbosity levels (quiet, normal, verbose) | ||||
| - ✅ Real-time output with cleaner formatting | ||||
| - ✅ Better error aggregation and display | ||||
| - ✅ TAP protocol support integrated with new logger | ||||
|  | ||||
| ## Completed changes | ||||
| ## Completed Improvements | ||||
|  | ||||
| ### 1. ✅ Update cli argument handling in index.ts | ||||
| - ✅ Detect argument type: file path, glob pattern, or directory | ||||
| - ✅ Check if argument contains glob characters (*, **, ?, [], etc.) | ||||
| - ✅ Pass appropriate mode to TsTest constructor | ||||
| - ✅ Added TestExecutionMode enum | ||||
| ### 1. ✅ Created new TsTestLogger class | ||||
| - ✅ Centralized logging with consistent formatting | ||||
| - ✅ Support for different output modes (normal, quiet, verbose) | ||||
| - ✅ Better visual hierarchy with modern Unicode characters | ||||
| - ✅ Progress indicators for multiple test files | ||||
| - ✅ Structured error collection and display | ||||
|  | ||||
| ### 2. ✅ Modify TsTest constructor and class | ||||
| - ✅ Add support for three modes: directory, file, glob | ||||
| - ✅ Update constructor to accept pattern/path and mode | ||||
| - ✅ Added executionMode property to track the mode | ||||
| ### 2. ✅ Updated visual design | ||||
| - ✅ Replaced heavy separators with cleaner alternatives | ||||
| - ✅ Used better emoji and Unicode characters | ||||
| - ✅ Added indentation for hierarchical display | ||||
| - ✅ Grouped related information visually | ||||
| - ✅ Added color coding consistently | ||||
|  | ||||
| ### 3. ✅ Update TestDirectory class | ||||
| - ✅ Used `listFileTree` for glob pattern support | ||||
| - ✅ Used `SmartFile.fromFilePath` for single file loading | ||||
| - ✅ Refactored to support all three modes in `_init` method | ||||
| - ✅ Return appropriate file array based on mode | ||||
| - ✅ Changed default directory behavior to recursive search | ||||
|   - ✅ When directory argument: use `**/test*.ts` pattern for recursive search | ||||
|   - ✅ This ensures subdirectories are included in test discovery | ||||
| ### 3. ✅ Added command-line options | ||||
| - ✅ `--quiet` for minimal CI-friendly output | ||||
| - ✅ `--verbose` for detailed debugging information | ||||
| - ✅ `--no-color` for environments without color support | ||||
| - ✅ `--json` for structured JSON output | ||||
| - ⏳ `--log-file <path>` for persistent logging (TODO) | ||||
|  | ||||
| ### 4. ✅ Test the implementation | ||||
| - ✅ Created test file `test/test.single.ts` for single file functionality | ||||
| - ✅ Created test file `test/test.glob.ts` for glob pattern functionality | ||||
| - ✅ Created test in subdirectory `test/subdir/test.sub.ts` for recursive search | ||||
| - ✅ Tested with existing test files for backward compatibility | ||||
| - ✅ Tested glob patterns: `test/test.*.ts` works correctly | ||||
| - ✅ Verified that default behavior now includes subdirectories | ||||
| ### 4. ✅ Improved progress feedback | ||||
| - ⏳ Show progress bar for multiple files (TODO) | ||||
| - ✅ Display current file being executed | ||||
| - ✅ Show real-time test counts | ||||
| - ⏳ Add ETA for long test suites (TODO) | ||||
|  | ||||
| ## Implementation completed | ||||
| 1. ✅ CLI argument type detection implemented | ||||
| 2. ✅ TsTest class supports all three modes | ||||
| 3. ✅ TestDirectory handles files, globs, and directories | ||||
| 4. ✅ Default pattern changed from `test*.ts` to `**/test*.ts` for recursive search | ||||
| 5. ✅ Comprehensive tests added and all modes verified | ||||
| ### 5. ✅ Better error and summary display | ||||
| - ✅ Collect all errors and display at end | ||||
| - ✅ Show timing metrics and performance summary (in verbose mode) | ||||
| - ✅ Highlight slowest tests (in verbose mode) | ||||
| - ✅ Add test failure context | ||||
|  | ||||
| ### 6. ✅ Browser console integration | ||||
| - ✅ Clearly separate browser logs from test output | ||||
| - ⏳ Add browser log filtering options (TODO) | ||||
| - ✅ Format browser errors specially | ||||
|  | ||||
| ## Implementation Steps - COMPLETED | ||||
|  | ||||
| ### Phase 1: ✅ Core Logger Implementation | ||||
| 1. ✅ Created `tstest.logging.ts` with TsTestLogger class | ||||
| 2. ✅ Added LogOptions interface for configuration | ||||
| 3. ✅ Implemented basic formatting methods | ||||
| 4. ✅ Added progress and summary methods | ||||
|  | ||||
| ### Phase 2: ✅ Integration | ||||
| 1. ✅ Updated CLI to accept new command-line options | ||||
| 2. ✅ Modified TsTest class to use new logger | ||||
| 3. ✅ Updated TapParser to use structured logging | ||||
| 4. ✅ Updated TapCombinator for better summaries | ||||
|  | ||||
| ### Phase 3: ✅ Visual Improvements | ||||
| 1. ✅ Replaced all existing separators | ||||
| 2. ✅ Updated color scheme | ||||
| 3. ✅ Added emoji and Unicode characters | ||||
| 4. ✅ Implemented hierarchical output | ||||
|  | ||||
| ### Phase 4: ✅ Advanced Features | ||||
| 1. ✅ Add JSON output format | ||||
| 2. ⏳ Implement file-based logging (TODO) | ||||
| 3. ✅ Add performance metrics collection | ||||
| 4. ✅ Create error aggregation system | ||||
|  | ||||
| ### Phase 5: ✅ Browser Integration | ||||
| 1. ✅ Update browser console forwarding | ||||
| 2. ✅ Add browser log formatting | ||||
| 3. ✅ Implement browser-specific indicators | ||||
|  | ||||
| ## Files modified | ||||
| - ✅ `ts/tstest.logging.ts` - New logger implementation (created) | ||||
| - ✅ `ts/index.ts` - Added CLI options parsing | ||||
| - ✅ `ts/tstest.classes.tstest.ts` - Integrated new logger | ||||
| - ✅ `ts/tstest.classes.tap.parser.ts` - Updated output formatting | ||||
| - ✅ `ts/tstest.classes.tap.combinator.ts` - Improved summary display | ||||
| - ❌ `ts/tstest.logprefixes.ts` - Still in use, can be deprecated later | ||||
| - ❌ `package.json` - No new dependencies needed | ||||
|  | ||||
| ## Success Criteria - ACHIEVED | ||||
| - ✅ Cleaner, more readable test output | ||||
| - ✅ Configurable verbosity levels | ||||
| - ✅ Better CI/CD integration | ||||
| - ✅ Improved error visibility | ||||
| - ✅ Performance metrics available | ||||
| - ✅ Consistent visual design | ||||
| - ✅ Maintained backward compatibility | ||||
|  | ||||
| ## Example Output Comparison | ||||
|  | ||||
| ### Current Output | ||||
| ``` | ||||
| ☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰ | ||||
| **TSTEST** FOUND 4 TESTFILE(S): | ||||
| **TSTEST** test/test.ts | ||||
| ------------------------------------------------ | ||||
| => Running test/test.ts in node.js runtime. | ||||
| = = = = = = = = = = = = = = = = = = = = = = = = | ||||
| ``` | ||||
|  | ||||
| ### Actual Output (IMPLEMENTED) | ||||
| ``` | ||||
| 🔍 Test Discovery | ||||
|    Mode: directory | ||||
|    Pattern: test | ||||
|    Found: 4 test file(s) | ||||
|  | ||||
| ▶️  test/test.ts (1/4) | ||||
|    Runtime: node.js | ||||
|    ✅ prepare test (0ms) | ||||
|    Summary: 1/1 PASSED | ||||
|  | ||||
| ▶️  test/test.single.ts (2/4) | ||||
|    Runtime: node.js | ||||
|    ✅ single file test execution (1ms) | ||||
|    Summary: 1/1 PASSED | ||||
|  | ||||
| 📊 Test Summary | ||||
| ┌────────────────────────────────┐ | ||||
| │ Total Files:                 4 │ | ||||
| │ Total Tests:                 4 │ | ||||
| │ Passed:                      4 │ | ||||
| │ Failed:                      0 │ | ||||
| │ Duration:                 5739ms │ | ||||
| └────────────────────────────────┘ | ||||
|  | ||||
| ALL TESTS PASSED! 🎉 | ||||
| ``` | ||||
|  | ||||
| ### Additional Features Implemented | ||||
|  | ||||
| 1. **Quiet Mode**: | ||||
| ``` | ||||
| Found 1 tests | ||||
| ✅ single file test execution | ||||
|  | ||||
| Summary: 1/1 | 1210ms | PASSED | ||||
| ``` | ||||
|  | ||||
| 2. **JSON Mode**: | ||||
| ```json | ||||
| {"event":"discovery","count":1,"pattern":"test/test.single.ts","executionMode":"file"} | ||||
| {"event":"fileStart","filename":"test/test.single.ts","runtime":"node.js","index":1,"total":1} | ||||
| {"event":"testResult","testName":"single file test execution","passed":true,"duration":0} | ||||
| {"event":"summary","summary":{"totalFiles":1,"totalTests":1,"totalPassed":1,"totalFailed":0,"totalDuration":1223,"fileResults":[...]}} | ||||
| ``` | ||||
|  | ||||
| 3. **Error Display**: | ||||
| ``` | ||||
| ❌ Failed Tests: | ||||
|  | ||||
|    test/test.fail.ts | ||||
|       ❌ This test should fail | ||||
|  | ||||
| SOME TESTS FAILED! ❌ | ||||
| ``` | ||||
|  | ||||
| ## Summary of Implementation | ||||
|  | ||||
| The logging improvement plan has been successfully implemented with the following achievements: | ||||
|  | ||||
| 1. **Created a new centralized TsTestLogger class** that handles all output formatting | ||||
| 2. **Added multiple output modes**: quiet, normal, verbose, and JSON | ||||
| 3. **Improved visual design** with modern Unicode characters and emojis | ||||
| 4. **Added CLI argument parsing** for all new options | ||||
| 5. **Integrated the logger throughout the codebase** (TsTest, TapParser, TapCombinator) | ||||
| 6. **Better error aggregation and display** with failed tests shown at the end | ||||
| 7. **Performance metrics** available in verbose mode | ||||
| 8. **Clean, hierarchical output** that's much more readable | ||||
|  | ||||
| ### Remaining TODOs | ||||
|  | ||||
| Only a few minor features remain unimplemented: | ||||
| - File-based logging (--log-file option) | ||||
| - Progress bar visualization | ||||
| - ETA for long test suites | ||||
| - Browser log filtering options | ||||
|  | ||||
| The core logging improvements are complete and provide a much better user experience! | ||||
							
								
								
									
										13
									
								
								test/test.fail.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								test/test.fail.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| import { expect, tap } from '@push.rocks/tapbundle'; | ||||
|  | ||||
| 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(); | ||||
| @@ -3,6 +3,6 @@ | ||||
|  */ | ||||
| export const commitinfo = { | ||||
|   name: '@git.zone/tstest', | ||||
|   version: '1.1.0', | ||||
|   version: '1.2.0', | ||||
|   description: 'a test utility to run tests that match test/**/*.ts' | ||||
| } | ||||
|   | ||||
							
								
								
									
										49
									
								
								ts/index.ts
									
									
									
									
									
								
							
							
						
						
									
										49
									
								
								ts/index.ts
									
									
									
									
									
								
							| @@ -1,4 +1,5 @@ | ||||
| import { TsTest } from './tstest.classes.tstest.js'; | ||||
| import type { LogOptions } from './tstest.logging.js'; | ||||
|  | ||||
| export enum TestExecutionMode { | ||||
|   DIRECTORY = 'directory', | ||||
| @@ -7,12 +8,54 @@ export enum TestExecutionMode { | ||||
| } | ||||
|  | ||||
| export const runCli = async () => { | ||||
|   if (!process.argv[2]) { | ||||
|   // Parse command line arguments | ||||
|   const args = process.argv.slice(2); | ||||
|   const logOptions: LogOptions = {}; | ||||
|   let testPath: string | null = null; | ||||
|    | ||||
|   // Parse options | ||||
|   for (let i = 0; i < args.length; i++) { | ||||
|     const arg = args[i]; | ||||
|      | ||||
|     switch (arg) { | ||||
|       case '--quiet': | ||||
|       case '-q': | ||||
|         logOptions.quiet = true; | ||||
|         break; | ||||
|       case '--verbose': | ||||
|       case '-v': | ||||
|         logOptions.verbose = true; | ||||
|         break; | ||||
|       case '--no-color': | ||||
|         logOptions.noColor = true; | ||||
|         break; | ||||
|       case '--json': | ||||
|         logOptions.json = true; | ||||
|         break; | ||||
|       case '--log-file': | ||||
|         if (i + 1 < args.length) { | ||||
|           logOptions.logFile = args[++i]; | ||||
|         } | ||||
|         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('  --log-file      Write logs to file'); | ||||
|     process.exit(1); | ||||
|   } | ||||
|    | ||||
|   const testPath = process.argv[2]; | ||||
|   let executionMode: TestExecutionMode; | ||||
|    | ||||
|   // Detect execution mode based on the argument | ||||
| @@ -24,6 +67,6 @@ export const runCli = async () => { | ||||
|     executionMode = TestExecutionMode.DIRECTORY; | ||||
|   } | ||||
|    | ||||
|   const tsTestInstance = new TsTest(process.cwd(), testPath, executionMode); | ||||
|   const tsTestInstance = new TsTest(process.cwd(), testPath, executionMode, logOptions); | ||||
|   await tsTestInstance.run(); | ||||
| }; | ||||
|   | ||||
| @@ -6,59 +6,37 @@ import { coloredString as cs } from '@push.rocks/consolecolor'; | ||||
|  | ||||
| import { TapParser } from './tstest.classes.tap.parser.js'; | ||||
| import * as logPrefixes from './tstest.logprefixes.js'; | ||||
| import { TsTestLogger } from './tstest.logging.js'; | ||||
|  | ||||
| export class TapCombinator { | ||||
|   tapParserStore: TapParser[] = []; | ||||
|   private logger: TsTestLogger; | ||||
|    | ||||
|   constructor(logger: TsTestLogger) { | ||||
|     this.logger = logger; | ||||
|   } | ||||
|    | ||||
|   addTapParser(tapParserArg: TapParser) { | ||||
|     this.tapParserStore.push(tapParserArg); | ||||
|   } | ||||
|  | ||||
|   evaluate() { | ||||
|     console.log( | ||||
|       `${logPrefixes.TsTestPrefix} RESULTS FOR ${this.tapParserStore.length} TESTFILE(S):` | ||||
|     ); | ||||
|  | ||||
|     let failGlobal = false; // determine wether tstest should fail | ||||
|     // Call the logger's summary method | ||||
|     this.logger.summary(); | ||||
|      | ||||
|     // Check for failures | ||||
|     let failGlobal = false; | ||||
|     for (const tapParser of this.tapParserStore) { | ||||
|       if (!tapParser.expectedTests) { | ||||
|       if (!tapParser.expectedTests ||  | ||||
|           tapParser.expectedTests !== tapParser.receivedTests ||  | ||||
|           tapParser.getErrorTests().length > 0) { | ||||
|         failGlobal = true; | ||||
|         let overviewString = | ||||
|           logPrefixes.TsTestPrefix + | ||||
|           cs(` ${tapParser.fileName} ${plugins.figures.cross}`, 'red') + | ||||
|           ` ${plugins.figures.pointer} ` + | ||||
|           `does not specify tests!`; | ||||
|         console.log(overviewString); | ||||
|       } else if (tapParser.expectedTests !== tapParser.receivedTests) { | ||||
|         failGlobal = true; | ||||
|         let overviewString = | ||||
|           logPrefixes.TsTestPrefix + | ||||
|           cs(` ${tapParser.fileName} ${plugins.figures.cross}`, 'red') + | ||||
|           ` ${plugins.figures.pointer} ` + | ||||
|           tapParser.getTestOverviewAsString() + | ||||
|           `did not execute all specified tests!`; | ||||
|         console.log(overviewString); | ||||
|       } else if (tapParser.getErrorTests().length === 0) { | ||||
|         let overviewString = | ||||
|           logPrefixes.TsTestPrefix + | ||||
|           cs(` ${tapParser.fileName} ${plugins.figures.tick}`, 'green') + | ||||
|           ` ${plugins.figures.pointer} ` + | ||||
|           tapParser.getTestOverviewAsString(); | ||||
|         console.log(overviewString); | ||||
|       } else { | ||||
|         failGlobal = true; | ||||
|         let overviewString = | ||||
|           logPrefixes.TsTestPrefix + | ||||
|           cs(` ${tapParser.fileName} ${plugins.figures.cross}`, 'red') + | ||||
|           ` ${plugins.figures.pointer} ` + | ||||
|           tapParser.getTestOverviewAsString(); | ||||
|         console.log(overviewString); | ||||
|         break; | ||||
|       } | ||||
|     } | ||||
|     console.log(cs(plugins.figures.hamburger.repeat(48), 'cyan')); | ||||
|     if (!failGlobal) { | ||||
|       console.log(cs('FINAL RESULT: SUCCESS!', 'green')); | ||||
|     } else { | ||||
|       console.log(cs('FINAL RESULT: FAIL!', 'red')); | ||||
|      | ||||
|     // Exit with error code if tests failed | ||||
|     if (failGlobal) { | ||||
|       process.exit(1); | ||||
|     } | ||||
|   } | ||||
|   | ||||
| @@ -7,6 +7,7 @@ import { coloredString as cs } from '@push.rocks/consolecolor'; | ||||
| import * as plugins from './tstest.plugins.js'; | ||||
| import { TapTestResult } from './tstest.classes.tap.testresult.js'; | ||||
| import * as logPrefixes from './tstest.logprefixes.js'; | ||||
| import { TsTestLogger } from './tstest.logging.js'; | ||||
|  | ||||
| export class TapParser { | ||||
|   testStore: TapTestResult[] = []; | ||||
| @@ -19,11 +20,15 @@ export class TapParser { | ||||
|   activeTapTestResult: TapTestResult; | ||||
|  | ||||
|   pretaskRegex = /^::__PRETASK:(.*)$/; | ||||
|    | ||||
|   private logger: TsTestLogger; | ||||
|  | ||||
|   /** | ||||
|    * the constructor for TapParser | ||||
|    */ | ||||
|   constructor(public fileName: string) {} | ||||
|   constructor(public fileName: string, logger?: TsTestLogger) { | ||||
|     this.logger = logger; | ||||
|   } | ||||
|  | ||||
|   private _getNewTapTestResult() { | ||||
|     this.activeTapTestResult = new TapTestResult(this.testStore.length + 1); | ||||
| @@ -45,9 +50,9 @@ export class TapParser { | ||||
|         logLineIsTapProtocol = true; | ||||
|         const regexResult = this.expectedTestsRegex.exec(logLine); | ||||
|         this.expectedTests = parseInt(regexResult[2]); | ||||
|         console.log( | ||||
|           `${logPrefixes.TapPrefix} ${cs(`Expecting ${this.expectedTests} tests!`, 'blue')}` | ||||
|         ); | ||||
|         if (this.logger) { | ||||
|           this.logger.tapOutput(`Expecting ${this.expectedTests} tests!`); | ||||
|         } | ||||
|  | ||||
|         // initiating first TapResult | ||||
|         this._getNewTapTestResult(); | ||||
| @@ -55,7 +60,9 @@ export class TapParser { | ||||
|         logLineIsTapProtocol = true; | ||||
|         const pretaskContentMatch = this.pretaskRegex.exec(logLine); | ||||
|         if (pretaskContentMatch && pretaskContentMatch[1]) { | ||||
|           console.log(`${logPrefixes.TapPretaskPrefix} Pretask ->${pretaskContentMatch[1]}: Success.`); | ||||
|           if (this.logger) { | ||||
|             this.logger.tapOutput(`Pretask -> ${pretaskContentMatch[1]}: Success.`); | ||||
|           } | ||||
|         } | ||||
|       } else if (this.testStatusRegex.test(logLine)) { | ||||
|         logLineIsTapProtocol = true; | ||||
| @@ -73,26 +80,20 @@ export class TapParser { | ||||
|  | ||||
|         // test for protocol error | ||||
|         if (testId !== this.activeTapTestResult.id) { | ||||
|           console.log( | ||||
|             `${logPrefixes.TapErrorPrefix} Something is strange! Test Ids are not equal!` | ||||
|           ); | ||||
|           if (this.logger) { | ||||
|             this.logger.error('Something is strange! Test Ids are not equal!'); | ||||
|           } | ||||
|         } | ||||
|         this.activeTapTestResult.setTestResult(testOk); | ||||
|  | ||||
|         if (testOk) { | ||||
|           console.log( | ||||
|             logPrefixes.TapPrefix, | ||||
|             `${cs(`T${testId} ${plugins.figures.tick}`, 'green')} ${plugins.figures.arrowRight} ` + | ||||
|               cs(testSubject, 'blue') + | ||||
|               ` | ${cs(`${testDuration} ms`, 'orange')}` | ||||
|           ); | ||||
|           if (this.logger) { | ||||
|             this.logger.testResult(testSubject, true, testDuration); | ||||
|           } | ||||
|         } else { | ||||
|           console.log( | ||||
|             logPrefixes.TapPrefix, | ||||
|             `${cs(`T${testId} ${plugins.figures.cross}`, 'red')} ${plugins.figures.arrowRight} ` + | ||||
|               cs(testSubject, 'blue') + | ||||
|               ` | ${cs(`${testDuration} ms`, 'orange')}` | ||||
|           ); | ||||
|           if (this.logger) { | ||||
|             this.logger.testResult(testSubject, false, testDuration); | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|  | ||||
| @@ -100,7 +101,9 @@ export class TapParser { | ||||
|         if (this.activeTapTestResult) { | ||||
|           this.activeTapTestResult.addLogLine(logLine); | ||||
|         } | ||||
|         console.log(logLine); | ||||
|         if (this.logger) { | ||||
|           this.logger.tapOutput(logLine); | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       if (this.activeTapTestResult && this.activeTapTestResult.testSettled) { | ||||
| @@ -172,38 +175,32 @@ export class TapParser { | ||||
|  | ||||
|     // check wether all tests ran | ||||
|     if (this.expectedTests === this.receivedTests) { | ||||
|       console.log( | ||||
|         `${logPrefixes.TapPrefix} ${cs( | ||||
|           `${this.receivedTests} out of ${this.expectedTests} Tests completed!`, | ||||
|           'green' | ||||
|         )}` | ||||
|       ); | ||||
|       if (this.logger) { | ||||
|         this.logger.tapOutput(`${this.receivedTests} out of ${this.expectedTests} Tests completed!`); | ||||
|       } | ||||
|     } else { | ||||
|       console.log( | ||||
|         `${logPrefixes.TapErrorPrefix} ${cs( | ||||
|           `Only ${this.receivedTests} out of ${this.expectedTests} completed!`, | ||||
|           'red' | ||||
|         )}` | ||||
|       ); | ||||
|       if (this.logger) { | ||||
|         this.logger.error(`Only ${this.receivedTests} out of ${this.expectedTests} completed!`); | ||||
|       } | ||||
|     } | ||||
|     if (!this.expectedTests) { | ||||
|       console.log(cs('Error: No tests were defined. Therefore the testfile failed!', 'red')); | ||||
|       if (this.logger) { | ||||
|         this.logger.error('No tests were defined. Therefore the testfile failed!'); | ||||
|       } | ||||
|     } else if (this.expectedTests !== this.receivedTests) { | ||||
|       console.log( | ||||
|         cs( | ||||
|           'Error: The amount of received tests and expectedTests is unequal! Therefore the testfile failed', | ||||
|           'red' | ||||
|         ) | ||||
|       ); | ||||
|       if (this.logger) { | ||||
|         this.logger.error('The amount of received tests and expectedTests is unequal! Therefore the testfile failed'); | ||||
|       } | ||||
|     } else if (this.getErrorTests().length === 0) { | ||||
|       console.log(`${logPrefixes.TapPrefix} ${cs(`All tests are successfull!!!`, 'green')}`); | ||||
|       if (this.logger) { | ||||
|         this.logger.tapOutput('All tests are successfull!!!'); | ||||
|         this.logger.testFileEnd(this.receivedTests, 0, 0); | ||||
|       } | ||||
|     } else { | ||||
|       console.log( | ||||
|         `${logPrefixes.TapPrefix} ${cs( | ||||
|           `${this.getErrorTests().length} tests threw an error!!!`, | ||||
|           'red' | ||||
|         )}` | ||||
|       ); | ||||
|       if (this.logger) { | ||||
|         this.logger.tapOutput(`${this.getErrorTests().length} tests threw an error!!!`, true); | ||||
|         this.logger.testFileEnd(this.receivedTests - this.getErrorTests().length, this.getErrorTests().length, 0); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -8,10 +8,13 @@ import { TestDirectory } from './tstest.classes.testdirectory.js'; | ||||
| import { TapCombinator } from './tstest.classes.tap.combinator.js'; | ||||
| import { TapParser } from './tstest.classes.tap.parser.js'; | ||||
| import { TestExecutionMode } from './index.js'; | ||||
| import { TsTestLogger } from './tstest.logging.js'; | ||||
| import type { LogOptions } from './tstest.logging.js'; | ||||
|  | ||||
| export class TsTest { | ||||
|   public testDir: TestDirectory; | ||||
|   public executionMode: TestExecutionMode; | ||||
|   public logger: TsTestLogger; | ||||
|  | ||||
|   public smartshellInstance = new plugins.smartshell.Smartshell({ | ||||
|     executor: 'bash', | ||||
| @@ -22,62 +25,57 @@ export class TsTest { | ||||
|  | ||||
|   public tsbundleInstance = new plugins.tsbundle.TsBundle(); | ||||
|  | ||||
|   constructor(cwdArg: string, testPathArg: string, executionModeArg: TestExecutionMode) { | ||||
|   constructor(cwdArg: string, testPathArg: string, executionModeArg: TestExecutionMode, logOptions: LogOptions = {}) { | ||||
|     this.executionMode = executionModeArg; | ||||
|     this.testDir = new TestDirectory(cwdArg, testPathArg, executionModeArg); | ||||
|     this.logger = new TsTestLogger(logOptions); | ||||
|   } | ||||
|  | ||||
|   async run() { | ||||
|     const fileNamesToRun: string[] = await this.testDir.getTestFilePathArray(); | ||||
|     console.log(cs(plugins.figures.hamburger.repeat(80), 'cyan')); | ||||
|     console.log(''); | ||||
|     console.log(`${logPrefixes.TsTestPrefix} FOUND ${fileNamesToRun.length} TESTFILE(S):`); | ||||
|     for (const fileName of fileNamesToRun) { | ||||
|       console.log(`${logPrefixes.TsTestPrefix} ${cs(fileName, 'orange')}`); | ||||
|     } | ||||
|     console.log('-'.repeat(48)); | ||||
|     console.log(''); // force new line | ||||
|      | ||||
|     // Log test discovery | ||||
|     this.logger.testDiscovery( | ||||
|       fileNamesToRun.length,  | ||||
|       this.testDir.testPath, | ||||
|       this.executionMode | ||||
|     ); | ||||
|  | ||||
|     const tapCombinator = new TapCombinator(); // lets create the TapCombinator | ||||
|     const tapCombinator = new TapCombinator(this.logger); // lets create the TapCombinator | ||||
|     let fileIndex = 0; | ||||
|     for (const fileNameArg of fileNamesToRun) { | ||||
|       fileIndex++; | ||||
|       switch (true) { | ||||
|         case process.env.CI && fileNameArg.includes('.nonci.'): | ||||
|           console.log('!!!!!!!!!!!'); | ||||
|           console.log( | ||||
|             `not running testfile ${fileNameArg}, since we are CI and file name includes '.nonci.' tag` | ||||
|           ); | ||||
|           console.log('!!!!!!!!!!!'); | ||||
|           this.logger.tapOutput(`Skipping ${fileNameArg} - marked as non-CI`); | ||||
|           break; | ||||
|         case fileNameArg.endsWith('.browser.ts') || fileNameArg.endsWith('.browser.nonci.ts'): | ||||
|           const tapParserBrowser = await this.runInChrome(fileNameArg); | ||||
|           const tapParserBrowser = await this.runInChrome(fileNameArg, fileIndex, fileNamesToRun.length); | ||||
|           tapCombinator.addTapParser(tapParserBrowser); | ||||
|           break; | ||||
|         case fileNameArg.endsWith('.both.ts') || fileNameArg.endsWith('.both.nonci.ts'): | ||||
|           console.log('>>>>>>> TEST PART 1: chrome'); | ||||
|           const tapParserBothBrowser = await this.runInChrome(fileNameArg); | ||||
|           this.logger.sectionStart('Part 1: Chrome'); | ||||
|           const tapParserBothBrowser = await this.runInChrome(fileNameArg, fileIndex, fileNamesToRun.length); | ||||
|           tapCombinator.addTapParser(tapParserBothBrowser); | ||||
|           console.log(cs(`|`.repeat(16), 'cyan')); | ||||
|           console.log(''); // force new line | ||||
|           console.log('>>>>>>> TEST PART 2: node'); | ||||
|           const tapParserBothNode = await this.runInNode(fileNameArg); | ||||
|           this.logger.sectionEnd(); | ||||
|            | ||||
|           this.logger.sectionStart('Part 2: Node'); | ||||
|           const tapParserBothNode = await this.runInNode(fileNameArg, fileIndex, fileNamesToRun.length); | ||||
|           tapCombinator.addTapParser(tapParserBothNode); | ||||
|           this.logger.sectionEnd(); | ||||
|           break; | ||||
|         default: | ||||
|           const tapParserNode = await this.runInNode(fileNameArg); | ||||
|           const tapParserNode = await this.runInNode(fileNameArg, fileIndex, fileNamesToRun.length); | ||||
|           tapCombinator.addTapParser(tapParserNode); | ||||
|           break; | ||||
|       } | ||||
|  | ||||
|       console.log(cs(`^`.repeat(16), 'cyan')); | ||||
|       console.log(''); // force new line | ||||
|     } | ||||
|     tapCombinator.evaluate(); | ||||
|   } | ||||
|  | ||||
|   public async runInNode(fileNameArg: string): Promise<TapParser> { | ||||
|     console.log(`${cs('=> ', 'blue')} Running ${cs(fileNameArg, 'orange')} in node.js runtime.`); | ||||
|     console.log(`${cs(`= `.repeat(32), 'cyan')}`); | ||||
|     const tapParser = new TapParser(fileNameArg + ':node'); | ||||
|   public async runInNode(fileNameArg: string, index: number, total: number): Promise<TapParser> { | ||||
|     this.logger.testFileStart(fileNameArg, 'node.js', index, total); | ||||
|     const tapParser = new TapParser(fileNameArg + ':node', this.logger); | ||||
|  | ||||
|     // tsrun options | ||||
|     let tsrunOptions = ''; | ||||
| @@ -92,9 +90,8 @@ export class TsTest { | ||||
|     return tapParser; | ||||
|   } | ||||
|  | ||||
|   public async runInChrome(fileNameArg: string): Promise<TapParser> { | ||||
|     console.log(`${cs('=> ', 'blue')} Running ${cs(fileNameArg, 'orange')} in chromium runtime.`); | ||||
|     console.log(`${cs(`= `.repeat(32), 'cyan')}`); | ||||
|   public async runInChrome(fileNameArg: string, index: number, total: number): Promise<TapParser> { | ||||
|     this.logger.testFileStart(fileNameArg, 'chromium', index, total); | ||||
|  | ||||
|     // lets get all our paths sorted | ||||
|     const tsbundleCacheDirPath = plugins.path.join(paths.cwd, './.nogit/tstest_cache'); | ||||
| @@ -133,11 +130,17 @@ export class TsTest { | ||||
|     await server.start(); | ||||
|  | ||||
|     // lets handle realtime comms | ||||
|     const tapParser = new TapParser(fileNameArg + ':chrome'); | ||||
|     const tapParser = new TapParser(fileNameArg + ':chrome', this.logger); | ||||
|     const wss = new plugins.ws.WebSocketServer({ port: 8080 }); | ||||
|     wss.on('connection', (ws) => { | ||||
|       ws.on('message', (message) => { | ||||
|         tapParser.handleTapLog(message.toString()); | ||||
|         const messageStr = message.toString(); | ||||
|         if (messageStr.startsWith('console:')) { | ||||
|           const [, level, ...messageParts] = messageStr.split(':'); | ||||
|           this.logger.browserConsole(messageParts.join(':'), level); | ||||
|         } else { | ||||
|           tapParser.handleTapLog(messageStr); | ||||
|         } | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|   | ||||
							
								
								
									
										285
									
								
								ts/tstest.logging.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										285
									
								
								ts/tstest.logging.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,285 @@ | ||||
| import { coloredString as cs } from '@push.rocks/consolecolor'; | ||||
| import * as plugins from './tstest.plugins.js'; | ||||
|  | ||||
| export interface LogOptions { | ||||
|   quiet?: boolean; | ||||
|   verbose?: boolean; | ||||
|   noColor?: boolean; | ||||
|   json?: boolean; | ||||
|   logFile?: string; | ||||
| } | ||||
|  | ||||
| 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; | ||||
|    | ||||
|   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) return; | ||||
|     console.log(message); | ||||
|      | ||||
|     if (this.options.logFile) { | ||||
|       // TODO: Implement file logging | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   // 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) { | ||||
|       console.log(JSON.stringify({ 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: [] | ||||
|     }; | ||||
|      | ||||
|     if (this.options.json) { | ||||
|       console.log(JSON.stringify({ 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.currentFileResult.duration += duration; | ||||
|     } | ||||
|      | ||||
|     if (this.options.json) { | ||||
|       console.log(JSON.stringify({ event: 'testResult', testName, passed, duration, error })); | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     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')); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   testFileEnd(passed: number, failed: number, duration: number) { | ||||
|     if (this.currentFileResult) { | ||||
|       this.fileResults.push(this.currentFileResult); | ||||
|       this.currentFileResult = null; | ||||
|     } | ||||
|      | ||||
|     if (this.options.json) { | ||||
|       console.log(JSON.stringify({ 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)); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   // TAP output forwarding | ||||
|   tapOutput(message: string, isError: boolean = false) { | ||||
|     if (this.options.json) return; | ||||
|      | ||||
|     if (this.options.verbose || isError) { | ||||
|       const prefix = isError ? '   ⚠️ ' : '   '; | ||||
|       const color = isError ? 'red' : 'dim'; | ||||
|       this.log(this.format(`${prefix}${message}`, color)); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   // Browser console | ||||
|   browserConsole(message: string, level: string = 'log') { | ||||
|     if (this.options.json) { | ||||
|       console.log(JSON.stringify({ 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)); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   // 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) { | ||||
|       console.log(JSON.stringify({ 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) { | ||||
|       console.log(JSON.stringify({ 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')); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user