@git.zone/tstest 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,59 +6,37 @@ import { coloredString as cs } from '@push.rocks/consolecolor';
6
6
 
7
7
  import { TapParser } from './tstest.classes.tap.parser.js';
8
8
  import * as logPrefixes from './tstest.logprefixes.js';
9
+ import { TsTestLogger } from './tstest.logging.js';
9
10
 
10
11
  export class TapCombinator {
11
12
  tapParserStore: TapParser[] = [];
13
+ private logger: TsTestLogger;
14
+
15
+ constructor(logger: TsTestLogger) {
16
+ this.logger = logger;
17
+ }
18
+
12
19
  addTapParser(tapParserArg: TapParser) {
13
20
  this.tapParserStore.push(tapParserArg);
14
21
  }
15
22
 
16
23
  evaluate() {
17
- console.log(
18
- `${logPrefixes.TsTestPrefix} RESULTS FOR ${this.tapParserStore.length} TESTFILE(S):`
19
- );
20
-
21
- let failGlobal = false; // determine wether tstest should fail
24
+ // Call the logger's summary method
25
+ this.logger.summary();
26
+
27
+ // Check for failures
28
+ let failGlobal = false;
22
29
  for (const tapParser of this.tapParserStore) {
23
- if (!tapParser.expectedTests) {
24
- failGlobal = true;
25
- let overviewString =
26
- logPrefixes.TsTestPrefix +
27
- cs(` ${tapParser.fileName} ${plugins.figures.cross}`, 'red') +
28
- ` ${plugins.figures.pointer} ` +
29
- `does not specify tests!`;
30
- console.log(overviewString);
31
- } else if (tapParser.expectedTests !== tapParser.receivedTests) {
32
- failGlobal = true;
33
- let overviewString =
34
- logPrefixes.TsTestPrefix +
35
- cs(` ${tapParser.fileName} ${plugins.figures.cross}`, 'red') +
36
- ` ${plugins.figures.pointer} ` +
37
- tapParser.getTestOverviewAsString() +
38
- `did not execute all specified tests!`;
39
- console.log(overviewString);
40
- } else if (tapParser.getErrorTests().length === 0) {
41
- let overviewString =
42
- logPrefixes.TsTestPrefix +
43
- cs(` ${tapParser.fileName} ${plugins.figures.tick}`, 'green') +
44
- ` ${plugins.figures.pointer} ` +
45
- tapParser.getTestOverviewAsString();
46
- console.log(overviewString);
47
- } else {
30
+ if (!tapParser.expectedTests ||
31
+ tapParser.expectedTests !== tapParser.receivedTests ||
32
+ tapParser.getErrorTests().length > 0) {
48
33
  failGlobal = true;
49
- let overviewString =
50
- logPrefixes.TsTestPrefix +
51
- cs(` ${tapParser.fileName} ${plugins.figures.cross}`, 'red') +
52
- ` ${plugins.figures.pointer} ` +
53
- tapParser.getTestOverviewAsString();
54
- console.log(overviewString);
34
+ break;
55
35
  }
56
36
  }
57
- console.log(cs(plugins.figures.hamburger.repeat(48), 'cyan'));
58
- if (!failGlobal) {
59
- console.log(cs('FINAL RESULT: SUCCESS!', 'green'));
60
- } else {
61
- console.log(cs('FINAL RESULT: FAIL!', 'red'));
37
+
38
+ // Exit with error code if tests failed
39
+ if (failGlobal) {
62
40
  process.exit(1);
63
41
  }
64
42
  }
@@ -7,6 +7,7 @@ import { coloredString as cs } from '@push.rocks/consolecolor';
7
7
  import * as plugins from './tstest.plugins.js';
8
8
  import { TapTestResult } from './tstest.classes.tap.testresult.js';
9
9
  import * as logPrefixes from './tstest.logprefixes.js';
10
+ import { TsTestLogger } from './tstest.logging.js';
10
11
 
11
12
  export class TapParser {
12
13
  testStore: TapTestResult[] = [];
@@ -19,11 +20,15 @@ export class TapParser {
19
20
  activeTapTestResult: TapTestResult;
20
21
 
21
22
  pretaskRegex = /^::__PRETASK:(.*)$/;
23
+
24
+ private logger: TsTestLogger;
22
25
 
23
26
  /**
24
27
  * the constructor for TapParser
25
28
  */
26
- constructor(public fileName: string) {}
29
+ constructor(public fileName: string, logger?: TsTestLogger) {
30
+ this.logger = logger;
31
+ }
27
32
 
28
33
  private _getNewTapTestResult() {
29
34
  this.activeTapTestResult = new TapTestResult(this.testStore.length + 1);
@@ -45,9 +50,9 @@ export class TapParser {
45
50
  logLineIsTapProtocol = true;
46
51
  const regexResult = this.expectedTestsRegex.exec(logLine);
47
52
  this.expectedTests = parseInt(regexResult[2]);
48
- console.log(
49
- `${logPrefixes.TapPrefix} ${cs(`Expecting ${this.expectedTests} tests!`, 'blue')}`
50
- );
53
+ if (this.logger) {
54
+ this.logger.tapOutput(`Expecting ${this.expectedTests} tests!`);
55
+ }
51
56
 
52
57
  // initiating first TapResult
53
58
  this._getNewTapTestResult();
@@ -55,7 +60,9 @@ export class TapParser {
55
60
  logLineIsTapProtocol = true;
56
61
  const pretaskContentMatch = this.pretaskRegex.exec(logLine);
57
62
  if (pretaskContentMatch && pretaskContentMatch[1]) {
58
- console.log(`${logPrefixes.TapPretaskPrefix} Pretask ->${pretaskContentMatch[1]}: Success.`);
63
+ if (this.logger) {
64
+ this.logger.tapOutput(`Pretask -> ${pretaskContentMatch[1]}: Success.`);
65
+ }
59
66
  }
60
67
  } else if (this.testStatusRegex.test(logLine)) {
61
68
  logLineIsTapProtocol = true;
@@ -73,26 +80,20 @@ export class TapParser {
73
80
 
74
81
  // test for protocol error
75
82
  if (testId !== this.activeTapTestResult.id) {
76
- console.log(
77
- `${logPrefixes.TapErrorPrefix} Something is strange! Test Ids are not equal!`
78
- );
83
+ if (this.logger) {
84
+ this.logger.error('Something is strange! Test Ids are not equal!');
85
+ }
79
86
  }
80
87
  this.activeTapTestResult.setTestResult(testOk);
81
88
 
82
89
  if (testOk) {
83
- console.log(
84
- logPrefixes.TapPrefix,
85
- `${cs(`T${testId} ${plugins.figures.tick}`, 'green')} ${plugins.figures.arrowRight} ` +
86
- cs(testSubject, 'blue') +
87
- ` | ${cs(`${testDuration} ms`, 'orange')}`
88
- );
90
+ if (this.logger) {
91
+ this.logger.testResult(testSubject, true, testDuration);
92
+ }
89
93
  } else {
90
- console.log(
91
- logPrefixes.TapPrefix,
92
- `${cs(`T${testId} ${plugins.figures.cross}`, 'red')} ${plugins.figures.arrowRight} ` +
93
- cs(testSubject, 'blue') +
94
- ` | ${cs(`${testDuration} ms`, 'orange')}`
95
- );
94
+ if (this.logger) {
95
+ this.logger.testResult(testSubject, false, testDuration);
96
+ }
96
97
  }
97
98
  }
98
99
 
@@ -100,7 +101,9 @@ export class TapParser {
100
101
  if (this.activeTapTestResult) {
101
102
  this.activeTapTestResult.addLogLine(logLine);
102
103
  }
103
- console.log(logLine);
104
+ if (this.logger) {
105
+ this.logger.tapOutput(logLine);
106
+ }
104
107
  }
105
108
 
106
109
  if (this.activeTapTestResult && this.activeTapTestResult.testSettled) {
@@ -172,38 +175,32 @@ export class TapParser {
172
175
 
173
176
  // check wether all tests ran
174
177
  if (this.expectedTests === this.receivedTests) {
175
- console.log(
176
- `${logPrefixes.TapPrefix} ${cs(
177
- `${this.receivedTests} out of ${this.expectedTests} Tests completed!`,
178
- 'green'
179
- )}`
180
- );
178
+ if (this.logger) {
179
+ this.logger.tapOutput(`${this.receivedTests} out of ${this.expectedTests} Tests completed!`);
180
+ }
181
181
  } else {
182
- console.log(
183
- `${logPrefixes.TapErrorPrefix} ${cs(
184
- `Only ${this.receivedTests} out of ${this.expectedTests} completed!`,
185
- 'red'
186
- )}`
187
- );
182
+ if (this.logger) {
183
+ this.logger.error(`Only ${this.receivedTests} out of ${this.expectedTests} completed!`);
184
+ }
188
185
  }
189
186
  if (!this.expectedTests) {
190
- console.log(cs('Error: No tests were defined. Therefore the testfile failed!', 'red'));
187
+ if (this.logger) {
188
+ this.logger.error('No tests were defined. Therefore the testfile failed!');
189
+ }
191
190
  } else if (this.expectedTests !== this.receivedTests) {
192
- console.log(
193
- cs(
194
- 'Error: The amount of received tests and expectedTests is unequal! Therefore the testfile failed',
195
- 'red'
196
- )
197
- );
191
+ if (this.logger) {
192
+ this.logger.error('The amount of received tests and expectedTests is unequal! Therefore the testfile failed');
193
+ }
198
194
  } else if (this.getErrorTests().length === 0) {
199
- console.log(`${logPrefixes.TapPrefix} ${cs(`All tests are successfull!!!`, 'green')}`);
195
+ if (this.logger) {
196
+ this.logger.tapOutput('All tests are successfull!!!');
197
+ this.logger.testFileEnd(this.receivedTests, 0, 0);
198
+ }
200
199
  } else {
201
- console.log(
202
- `${logPrefixes.TapPrefix} ${cs(
203
- `${this.getErrorTests().length} tests threw an error!!!`,
204
- 'red'
205
- )}`
206
- );
200
+ if (this.logger) {
201
+ this.logger.tapOutput(`${this.getErrorTests().length} tests threw an error!!!`, true);
202
+ this.logger.testFileEnd(this.receivedTests - this.getErrorTests().length, this.getErrorTests().length, 0);
203
+ }
207
204
  }
208
205
  }
209
206
  }
@@ -8,10 +8,13 @@ import { TestDirectory } from './tstest.classes.testdirectory.js';
8
8
  import { TapCombinator } from './tstest.classes.tap.combinator.js';
9
9
  import { TapParser } from './tstest.classes.tap.parser.js';
10
10
  import { TestExecutionMode } from './index.js';
11
+ import { TsTestLogger } from './tstest.logging.js';
12
+ import type { LogOptions } from './tstest.logging.js';
11
13
 
12
14
  export class TsTest {
13
15
  public testDir: TestDirectory;
14
16
  public executionMode: TestExecutionMode;
17
+ public logger: TsTestLogger;
15
18
 
16
19
  public smartshellInstance = new plugins.smartshell.Smartshell({
17
20
  executor: 'bash',
@@ -22,62 +25,57 @@ export class TsTest {
22
25
 
23
26
  public tsbundleInstance = new plugins.tsbundle.TsBundle();
24
27
 
25
- constructor(cwdArg: string, testPathArg: string, executionModeArg: TestExecutionMode) {
28
+ constructor(cwdArg: string, testPathArg: string, executionModeArg: TestExecutionMode, logOptions: LogOptions = {}) {
26
29
  this.executionMode = executionModeArg;
27
30
  this.testDir = new TestDirectory(cwdArg, testPathArg, executionModeArg);
31
+ this.logger = new TsTestLogger(logOptions);
28
32
  }
29
33
 
30
34
  async run() {
31
35
  const fileNamesToRun: string[] = await this.testDir.getTestFilePathArray();
32
- console.log(cs(plugins.figures.hamburger.repeat(80), 'cyan'));
33
- console.log('');
34
- console.log(`${logPrefixes.TsTestPrefix} FOUND ${fileNamesToRun.length} TESTFILE(S):`);
35
- for (const fileName of fileNamesToRun) {
36
- console.log(`${logPrefixes.TsTestPrefix} ${cs(fileName, 'orange')}`);
37
- }
38
- console.log('-'.repeat(48));
39
- console.log(''); // force new line
36
+
37
+ // Log test discovery
38
+ this.logger.testDiscovery(
39
+ fileNamesToRun.length,
40
+ this.testDir.testPath,
41
+ this.executionMode
42
+ );
40
43
 
41
- const tapCombinator = new TapCombinator(); // lets create the TapCombinator
44
+ const tapCombinator = new TapCombinator(this.logger); // lets create the TapCombinator
45
+ let fileIndex = 0;
42
46
  for (const fileNameArg of fileNamesToRun) {
47
+ fileIndex++;
43
48
  switch (true) {
44
49
  case process.env.CI && fileNameArg.includes('.nonci.'):
45
- console.log('!!!!!!!!!!!');
46
- console.log(
47
- `not running testfile ${fileNameArg}, since we are CI and file name includes '.nonci.' tag`
48
- );
49
- console.log('!!!!!!!!!!!');
50
+ this.logger.tapOutput(`Skipping ${fileNameArg} - marked as non-CI`);
50
51
  break;
51
52
  case fileNameArg.endsWith('.browser.ts') || fileNameArg.endsWith('.browser.nonci.ts'):
52
- const tapParserBrowser = await this.runInChrome(fileNameArg);
53
+ const tapParserBrowser = await this.runInChrome(fileNameArg, fileIndex, fileNamesToRun.length);
53
54
  tapCombinator.addTapParser(tapParserBrowser);
54
55
  break;
55
56
  case fileNameArg.endsWith('.both.ts') || fileNameArg.endsWith('.both.nonci.ts'):
56
- console.log('>>>>>>> TEST PART 1: chrome');
57
- const tapParserBothBrowser = await this.runInChrome(fileNameArg);
57
+ this.logger.sectionStart('Part 1: Chrome');
58
+ const tapParserBothBrowser = await this.runInChrome(fileNameArg, fileIndex, fileNamesToRun.length);
58
59
  tapCombinator.addTapParser(tapParserBothBrowser);
59
- console.log(cs(`|`.repeat(16), 'cyan'));
60
- console.log(''); // force new line
61
- console.log('>>>>>>> TEST PART 2: node');
62
- const tapParserBothNode = await this.runInNode(fileNameArg);
60
+ this.logger.sectionEnd();
61
+
62
+ this.logger.sectionStart('Part 2: Node');
63
+ const tapParserBothNode = await this.runInNode(fileNameArg, fileIndex, fileNamesToRun.length);
63
64
  tapCombinator.addTapParser(tapParserBothNode);
65
+ this.logger.sectionEnd();
64
66
  break;
65
67
  default:
66
- const tapParserNode = await this.runInNode(fileNameArg);
68
+ const tapParserNode = await this.runInNode(fileNameArg, fileIndex, fileNamesToRun.length);
67
69
  tapCombinator.addTapParser(tapParserNode);
68
70
  break;
69
71
  }
70
-
71
- console.log(cs(`^`.repeat(16), 'cyan'));
72
- console.log(''); // force new line
73
72
  }
74
73
  tapCombinator.evaluate();
75
74
  }
76
75
 
77
- public async runInNode(fileNameArg: string): Promise<TapParser> {
78
- console.log(`${cs('=> ', 'blue')} Running ${cs(fileNameArg, 'orange')} in node.js runtime.`);
79
- console.log(`${cs(`= `.repeat(32), 'cyan')}`);
80
- const tapParser = new TapParser(fileNameArg + ':node');
76
+ public async runInNode(fileNameArg: string, index: number, total: number): Promise<TapParser> {
77
+ this.logger.testFileStart(fileNameArg, 'node.js', index, total);
78
+ const tapParser = new TapParser(fileNameArg + ':node', this.logger);
81
79
 
82
80
  // tsrun options
83
81
  let tsrunOptions = '';
@@ -92,9 +90,8 @@ export class TsTest {
92
90
  return tapParser;
93
91
  }
94
92
 
95
- public async runInChrome(fileNameArg: string): Promise<TapParser> {
96
- console.log(`${cs('=> ', 'blue')} Running ${cs(fileNameArg, 'orange')} in chromium runtime.`);
97
- console.log(`${cs(`= `.repeat(32), 'cyan')}`);
93
+ public async runInChrome(fileNameArg: string, index: number, total: number): Promise<TapParser> {
94
+ this.logger.testFileStart(fileNameArg, 'chromium', index, total);
98
95
 
99
96
  // lets get all our paths sorted
100
97
  const tsbundleCacheDirPath = plugins.path.join(paths.cwd, './.nogit/tstest_cache');
@@ -133,11 +130,17 @@ export class TsTest {
133
130
  await server.start();
134
131
 
135
132
  // lets handle realtime comms
136
- const tapParser = new TapParser(fileNameArg + ':chrome');
133
+ const tapParser = new TapParser(fileNameArg + ':chrome', this.logger);
137
134
  const wss = new plugins.ws.WebSocketServer({ port: 8080 });
138
135
  wss.on('connection', (ws) => {
139
136
  ws.on('message', (message) => {
140
- tapParser.handleTapLog(message.toString());
137
+ const messageStr = message.toString();
138
+ if (messageStr.startsWith('console:')) {
139
+ const [, level, ...messageParts] = messageStr.split(':');
140
+ this.logger.browserConsole(messageParts.join(':'), level);
141
+ } else {
142
+ tapParser.handleTapLog(messageStr);
143
+ }
141
144
  });
142
145
  });
143
146
 
@@ -0,0 +1,285 @@
1
+ import { coloredString as cs } from '@push.rocks/consolecolor';
2
+ import * as plugins from './tstest.plugins.js';
3
+
4
+ export interface LogOptions {
5
+ quiet?: boolean;
6
+ verbose?: boolean;
7
+ noColor?: boolean;
8
+ json?: boolean;
9
+ logFile?: string;
10
+ }
11
+
12
+ export interface TestFileResult {
13
+ file: string;
14
+ passed: number;
15
+ failed: number;
16
+ total: number;
17
+ duration: number;
18
+ tests: Array<{
19
+ name: string;
20
+ passed: boolean;
21
+ duration: number;
22
+ error?: string;
23
+ }>;
24
+ }
25
+
26
+ export interface TestSummary {
27
+ totalFiles: number;
28
+ totalTests: number;
29
+ totalPassed: number;
30
+ totalFailed: number;
31
+ totalDuration: number;
32
+ fileResults: TestFileResult[];
33
+ }
34
+
35
+ export class TsTestLogger {
36
+ private options: LogOptions;
37
+ private startTime: number;
38
+ private fileResults: TestFileResult[] = [];
39
+ private currentFileResult: TestFileResult | null = null;
40
+
41
+ constructor(options: LogOptions = {}) {
42
+ this.options = options;
43
+ this.startTime = Date.now();
44
+ }
45
+
46
+ private format(text: string, color?: string): string {
47
+ if (this.options.noColor || !color) {
48
+ return text;
49
+ }
50
+ return cs(text, color as any);
51
+ }
52
+
53
+ private log(message: string) {
54
+ if (this.options.json) return;
55
+ console.log(message);
56
+
57
+ if (this.options.logFile) {
58
+ // TODO: Implement file logging
59
+ }
60
+ }
61
+
62
+ // Section separators
63
+ sectionStart(title: string) {
64
+ if (this.options.quiet || this.options.json) return;
65
+ this.log(this.format(`\n━━━ ${title} ━━━`, 'cyan'));
66
+ }
67
+
68
+ sectionEnd() {
69
+ if (this.options.quiet || this.options.json) return;
70
+ this.log(this.format('─'.repeat(50), 'dim'));
71
+ }
72
+
73
+ // Progress indication
74
+ progress(current: number, total: number, message: string) {
75
+ if (this.options.quiet || this.options.json) return;
76
+ const percentage = Math.round((current / total) * 100);
77
+ const filled = Math.round((current / total) * 20);
78
+ const empty = 20 - filled;
79
+
80
+ this.log(this.format(`\n📊 Progress: ${current}/${total} (${percentage}%)`, 'cyan'));
81
+ this.log(this.format(`[${'█'.repeat(filled)}${'░'.repeat(empty)}] ${message}`, 'dim'));
82
+ }
83
+
84
+ // Test discovery
85
+ testDiscovery(count: number, pattern: string, executionMode: string) {
86
+ if (this.options.json) {
87
+ console.log(JSON.stringify({ event: 'discovery', count, pattern, executionMode }));
88
+ return;
89
+ }
90
+
91
+ if (this.options.quiet) {
92
+ this.log(`Found ${count} tests`);
93
+ } else {
94
+ this.log(this.format(`\n🔍 Test Discovery`, 'bold'));
95
+ this.log(this.format(` Mode: ${executionMode}`, 'dim'));
96
+ this.log(this.format(` Pattern: ${pattern}`, 'dim'));
97
+ this.log(this.format(` Found: ${count} test file(s)`, 'green'));
98
+ }
99
+ }
100
+
101
+ // Test execution
102
+ testFileStart(filename: string, runtime: string, index: number, total: number) {
103
+ this.currentFileResult = {
104
+ file: filename,
105
+ passed: 0,
106
+ failed: 0,
107
+ total: 0,
108
+ duration: 0,
109
+ tests: []
110
+ };
111
+
112
+ if (this.options.json) {
113
+ console.log(JSON.stringify({ event: 'fileStart', filename, runtime, index, total }));
114
+ return;
115
+ }
116
+
117
+ if (this.options.quiet) return;
118
+
119
+ this.log(this.format(`\n▶️ ${filename} (${index}/${total})`, 'blue'));
120
+ this.log(this.format(` Runtime: ${runtime}`, 'dim'));
121
+ }
122
+
123
+ testResult(testName: string, passed: boolean, duration: number, error?: string) {
124
+ if (this.currentFileResult) {
125
+ this.currentFileResult.tests.push({ name: testName, passed, duration, error });
126
+ this.currentFileResult.total++;
127
+ if (passed) {
128
+ this.currentFileResult.passed++;
129
+ } else {
130
+ this.currentFileResult.failed++;
131
+ }
132
+ this.currentFileResult.duration += duration;
133
+ }
134
+
135
+ if (this.options.json) {
136
+ console.log(JSON.stringify({ event: 'testResult', testName, passed, duration, error }));
137
+ return;
138
+ }
139
+
140
+ const icon = passed ? '✅' : '❌';
141
+ const color = passed ? 'green' : 'red';
142
+
143
+ if (this.options.quiet) {
144
+ this.log(`${icon} ${testName}`);
145
+ } else {
146
+ this.log(this.format(` ${icon} ${testName} (${duration}ms)`, color));
147
+ if (error && !passed) {
148
+ this.log(this.format(` ${error}`, 'red'));
149
+ }
150
+ }
151
+ }
152
+
153
+ testFileEnd(passed: number, failed: number, duration: number) {
154
+ if (this.currentFileResult) {
155
+ this.fileResults.push(this.currentFileResult);
156
+ this.currentFileResult = null;
157
+ }
158
+
159
+ if (this.options.json) {
160
+ console.log(JSON.stringify({ event: 'fileEnd', passed, failed, duration }));
161
+ return;
162
+ }
163
+
164
+ if (!this.options.quiet) {
165
+ const total = passed + failed;
166
+ const status = failed === 0 ? 'PASSED' : 'FAILED';
167
+ const color = failed === 0 ? 'green' : 'red';
168
+ this.log(this.format(` Summary: ${passed}/${total} ${status}`, color));
169
+ }
170
+ }
171
+
172
+ // TAP output forwarding
173
+ tapOutput(message: string, isError: boolean = false) {
174
+ if (this.options.json) return;
175
+
176
+ if (this.options.verbose || isError) {
177
+ const prefix = isError ? ' ⚠️ ' : ' ';
178
+ const color = isError ? 'red' : 'dim';
179
+ this.log(this.format(`${prefix}${message}`, color));
180
+ }
181
+ }
182
+
183
+ // Browser console
184
+ browserConsole(message: string, level: string = 'log') {
185
+ if (this.options.json) {
186
+ console.log(JSON.stringify({ event: 'browserConsole', message, level }));
187
+ return;
188
+ }
189
+
190
+ if (!this.options.quiet) {
191
+ const prefix = level === 'error' ? '🌐❌' : '🌐';
192
+ const color = level === 'error' ? 'red' : 'magenta';
193
+ this.log(this.format(` ${prefix} ${message}`, color));
194
+ }
195
+ }
196
+
197
+ // Final summary
198
+ summary() {
199
+ const totalDuration = Date.now() - this.startTime;
200
+ const summary: TestSummary = {
201
+ totalFiles: this.fileResults.length,
202
+ totalTests: this.fileResults.reduce((sum, r) => sum + r.total, 0),
203
+ totalPassed: this.fileResults.reduce((sum, r) => sum + r.passed, 0),
204
+ totalFailed: this.fileResults.reduce((sum, r) => sum + r.failed, 0),
205
+ totalDuration,
206
+ fileResults: this.fileResults
207
+ };
208
+
209
+ if (this.options.json) {
210
+ console.log(JSON.stringify({ event: 'summary', summary }));
211
+ return;
212
+ }
213
+
214
+ if (this.options.quiet) {
215
+ const status = summary.totalFailed === 0 ? 'PASSED' : 'FAILED';
216
+ this.log(`\nSummary: ${summary.totalPassed}/${summary.totalTests} | ${totalDuration}ms | ${status}`);
217
+ return;
218
+ }
219
+
220
+ // Detailed summary
221
+ this.log(this.format('\n📊 Test Summary', 'bold'));
222
+ this.log(this.format('┌────────────────────────────────┐', 'dim'));
223
+ this.log(this.format(`│ Total Files: ${summary.totalFiles.toString().padStart(14)} │`, 'white'));
224
+ this.log(this.format(`│ Total Tests: ${summary.totalTests.toString().padStart(14)} │`, 'white'));
225
+ this.log(this.format(`│ Passed: ${summary.totalPassed.toString().padStart(14)} │`, 'green'));
226
+ this.log(this.format(`│ Failed: ${summary.totalFailed.toString().padStart(14)} │`, summary.totalFailed > 0 ? 'red' : 'green'));
227
+ this.log(this.format(`│ Duration: ${totalDuration.toString().padStart(14)}ms │`, 'white'));
228
+ this.log(this.format('└────────────────────────────────┘', 'dim'));
229
+
230
+ // File results
231
+ if (summary.totalFailed > 0) {
232
+ this.log(this.format('\n❌ Failed Tests:', 'red'));
233
+ this.fileResults.forEach(fileResult => {
234
+ if (fileResult.failed > 0) {
235
+ this.log(this.format(`\n ${fileResult.file}`, 'yellow'));
236
+ fileResult.tests.filter(t => !t.passed).forEach(test => {
237
+ this.log(this.format(` ❌ ${test.name}`, 'red'));
238
+ if (test.error) {
239
+ this.log(this.format(` ${test.error}`, 'dim'));
240
+ }
241
+ });
242
+ }
243
+ });
244
+ }
245
+
246
+ // Performance metrics
247
+ if (this.options.verbose) {
248
+ const avgDuration = Math.round(totalDuration / summary.totalTests);
249
+ const slowestTest = this.fileResults
250
+ .flatMap(r => r.tests)
251
+ .sort((a, b) => b.duration - a.duration)[0];
252
+
253
+ this.log(this.format('\n⏱️ Performance Metrics:', 'cyan'));
254
+ this.log(this.format(` Average per test: ${avgDuration}ms`, 'white'));
255
+ if (slowestTest) {
256
+ this.log(this.format(` Slowest test: ${slowestTest.name} (${slowestTest.duration}ms)`, 'yellow'));
257
+ }
258
+ }
259
+
260
+ // Final status
261
+ const status = summary.totalFailed === 0 ? 'ALL TESTS PASSED! 🎉' : 'SOME TESTS FAILED! ❌';
262
+ const statusColor = summary.totalFailed === 0 ? 'green' : 'red';
263
+ this.log(this.format(`\n${status}`, statusColor));
264
+ }
265
+
266
+ // Error display
267
+ error(message: string, file?: string, stack?: string) {
268
+ if (this.options.json) {
269
+ console.log(JSON.stringify({ event: 'error', message, file, stack }));
270
+ return;
271
+ }
272
+
273
+ if (this.options.quiet) {
274
+ console.error(`ERROR: ${message}`);
275
+ } else {
276
+ this.log(this.format('\n⚠️ Error', 'red'));
277
+ if (file) this.log(this.format(` File: ${file}`, 'yellow'));
278
+ this.log(this.format(` ${message}`, 'red'));
279
+ if (stack && this.options.verbose) {
280
+ this.log(this.format(` Stack:`, 'dim'));
281
+ this.log(this.format(stack.split('\n').map(line => ` ${line}`).join('\n'), 'dim'));
282
+ }
283
+ }
284
+ }
285
+ }