@git.zone/tstest 1.0.96 → 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.
@@ -7,9 +7,14 @@ import { coloredString as cs } from '@push.rocks/consolecolor';
7
7
  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
+ import { TestExecutionMode } from './index.js';
11
+ import { TsTestLogger } from './tstest.logging.js';
12
+ import type { LogOptions } from './tstest.logging.js';
10
13
 
11
14
  export class TsTest {
12
15
  public testDir: TestDirectory;
16
+ public executionMode: TestExecutionMode;
17
+ public logger: TsTestLogger;
13
18
 
14
19
  public smartshellInstance = new plugins.smartshell.Smartshell({
15
20
  executor: 'bash',
@@ -20,61 +25,57 @@ export class TsTest {
20
25
 
21
26
  public tsbundleInstance = new plugins.tsbundle.TsBundle();
22
27
 
23
- constructor(cwdArg: string, relativePathToTestDirectory: string) {
24
- this.testDir = new TestDirectory(cwdArg, relativePathToTestDirectory);
28
+ constructor(cwdArg: string, testPathArg: string, executionModeArg: TestExecutionMode, logOptions: LogOptions = {}) {
29
+ this.executionMode = executionModeArg;
30
+ this.testDir = new TestDirectory(cwdArg, testPathArg, executionModeArg);
31
+ this.logger = new TsTestLogger(logOptions);
25
32
  }
26
33
 
27
34
  async run() {
28
35
  const fileNamesToRun: string[] = await this.testDir.getTestFilePathArray();
29
- console.log(cs(plugins.figures.hamburger.repeat(80), 'cyan'));
30
- console.log('');
31
- console.log(`${logPrefixes.TsTestPrefix} FOUND ${fileNamesToRun.length} TESTFILE(S):`);
32
- for (const fileName of fileNamesToRun) {
33
- console.log(`${logPrefixes.TsTestPrefix} ${cs(fileName, 'orange')}`);
34
- }
35
- console.log('-'.repeat(48));
36
- 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
+ );
37
43
 
38
- const tapCombinator = new TapCombinator(); // lets create the TapCombinator
44
+ const tapCombinator = new TapCombinator(this.logger); // lets create the TapCombinator
45
+ let fileIndex = 0;
39
46
  for (const fileNameArg of fileNamesToRun) {
47
+ fileIndex++;
40
48
  switch (true) {
41
49
  case process.env.CI && fileNameArg.includes('.nonci.'):
42
- console.log('!!!!!!!!!!!');
43
- console.log(
44
- `not running testfile ${fileNameArg}, since we are CI and file name includes '.nonci.' tag`
45
- );
46
- console.log('!!!!!!!!!!!');
50
+ this.logger.tapOutput(`Skipping ${fileNameArg} - marked as non-CI`);
47
51
  break;
48
52
  case fileNameArg.endsWith('.browser.ts') || fileNameArg.endsWith('.browser.nonci.ts'):
49
- const tapParserBrowser = await this.runInChrome(fileNameArg);
53
+ const tapParserBrowser = await this.runInChrome(fileNameArg, fileIndex, fileNamesToRun.length);
50
54
  tapCombinator.addTapParser(tapParserBrowser);
51
55
  break;
52
56
  case fileNameArg.endsWith('.both.ts') || fileNameArg.endsWith('.both.nonci.ts'):
53
- console.log('>>>>>>> TEST PART 1: chrome');
54
- const tapParserBothBrowser = await this.runInChrome(fileNameArg);
57
+ this.logger.sectionStart('Part 1: Chrome');
58
+ const tapParserBothBrowser = await this.runInChrome(fileNameArg, fileIndex, fileNamesToRun.length);
55
59
  tapCombinator.addTapParser(tapParserBothBrowser);
56
- console.log(cs(`|`.repeat(16), 'cyan'));
57
- console.log(''); // force new line
58
- console.log('>>>>>>> TEST PART 2: node');
59
- 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);
60
64
  tapCombinator.addTapParser(tapParserBothNode);
65
+ this.logger.sectionEnd();
61
66
  break;
62
67
  default:
63
- const tapParserNode = await this.runInNode(fileNameArg);
68
+ const tapParserNode = await this.runInNode(fileNameArg, fileIndex, fileNamesToRun.length);
64
69
  tapCombinator.addTapParser(tapParserNode);
65
70
  break;
66
71
  }
67
-
68
- console.log(cs(`^`.repeat(16), 'cyan'));
69
- console.log(''); // force new line
70
72
  }
71
73
  tapCombinator.evaluate();
72
74
  }
73
75
 
74
- public async runInNode(fileNameArg: string): Promise<TapParser> {
75
- console.log(`${cs('=> ', 'blue')} Running ${cs(fileNameArg, 'orange')} in node.js runtime.`);
76
- console.log(`${cs(`= `.repeat(32), 'cyan')}`);
77
- 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);
78
79
 
79
80
  // tsrun options
80
81
  let tsrunOptions = '';
@@ -89,9 +90,8 @@ export class TsTest {
89
90
  return tapParser;
90
91
  }
91
92
 
92
- public async runInChrome(fileNameArg: string): Promise<TapParser> {
93
- console.log(`${cs('=> ', 'blue')} Running ${cs(fileNameArg, 'orange')} in chromium runtime.`);
94
- 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);
95
95
 
96
96
  // lets get all our paths sorted
97
97
  const tsbundleCacheDirPath = plugins.path.join(paths.cwd, './.nogit/tstest_cache');
@@ -130,11 +130,17 @@ export class TsTest {
130
130
  await server.start();
131
131
 
132
132
  // lets handle realtime comms
133
- const tapParser = new TapParser(fileNameArg + ':chrome');
133
+ const tapParser = new TapParser(fileNameArg + ':chrome', this.logger);
134
134
  const wss = new plugins.ws.WebSocketServer({ port: 8080 });
135
135
  wss.on('connection', (ws) => {
136
136
  ws.on('message', (message) => {
137
- 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
+ }
138
144
  });
139
145
  });
140
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
+ }