@git.zone/tstest 1.1.0 ā 1.3.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.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/index.js +42 -4
- package/dist_ts/tstest.classes.tap.combinator.d.ts +3 -0
- package/dist_ts/tstest.classes.tap.combinator.js +14 -40
- package/dist_ts/tstest.classes.tap.parser.d.ts +3 -1
- package/dist_ts/tstest.classes.tap.parser.js +43 -18
- package/dist_ts/tstest.classes.tstest.d.ts +6 -3
- package/dist_ts/tstest.classes.tstest.js +32 -33
- package/dist_ts/tstest.logging.d.ts +53 -0
- package/dist_ts/tstest.logging.js +288 -0
- package/package.json +1 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/index.ts +45 -3
- package/ts/tstest.classes.tap.combinator.ts +19 -41
- package/ts/tstest.classes.tap.parser.ts +45 -47
- package/ts/tstest.classes.tstest.ts +38 -35
- package/ts/tstest.logging.ts +358 -0
- package/readme.plan.md +0 -51
|
@@ -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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,358 @@
|
|
|
1
|
+
import { coloredString as cs } from '@push.rocks/consolecolor';
|
|
2
|
+
import * as plugins from './tstest.plugins.js';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
|
|
6
|
+
export interface LogOptions {
|
|
7
|
+
quiet?: boolean;
|
|
8
|
+
verbose?: boolean;
|
|
9
|
+
noColor?: boolean;
|
|
10
|
+
json?: boolean;
|
|
11
|
+
logFile?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface TestFileResult {
|
|
15
|
+
file: string;
|
|
16
|
+
passed: number;
|
|
17
|
+
failed: number;
|
|
18
|
+
total: number;
|
|
19
|
+
duration: number;
|
|
20
|
+
tests: Array<{
|
|
21
|
+
name: string;
|
|
22
|
+
passed: boolean;
|
|
23
|
+
duration: number;
|
|
24
|
+
error?: string;
|
|
25
|
+
}>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface TestSummary {
|
|
29
|
+
totalFiles: number;
|
|
30
|
+
totalTests: number;
|
|
31
|
+
totalPassed: number;
|
|
32
|
+
totalFailed: number;
|
|
33
|
+
totalDuration: number;
|
|
34
|
+
fileResults: TestFileResult[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class TsTestLogger {
|
|
38
|
+
private options: LogOptions;
|
|
39
|
+
private startTime: number;
|
|
40
|
+
private fileResults: TestFileResult[] = [];
|
|
41
|
+
private currentFileResult: TestFileResult | null = null;
|
|
42
|
+
private currentTestLogFile: string | null = null;
|
|
43
|
+
|
|
44
|
+
constructor(options: LogOptions = {}) {
|
|
45
|
+
this.options = options;
|
|
46
|
+
this.startTime = Date.now();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private format(text: string, color?: string): string {
|
|
50
|
+
if (this.options.noColor || !color) {
|
|
51
|
+
return text;
|
|
52
|
+
}
|
|
53
|
+
return cs(text, color as any);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private log(message: string) {
|
|
57
|
+
if (this.options.json) {
|
|
58
|
+
// For JSON mode, skip console output
|
|
59
|
+
// JSON output is handled by logJson method
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
console.log(message);
|
|
64
|
+
|
|
65
|
+
// Log to the current test file log if we're in a test and --logfile is specified
|
|
66
|
+
if (this.currentTestLogFile) {
|
|
67
|
+
this.logToTestFile(message);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private logToFile(message: string) {
|
|
72
|
+
// This method is no longer used since we use logToTestFile for individual test logs
|
|
73
|
+
// Keeping it for potential future use with a global log file
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private logToTestFile(message: string) {
|
|
77
|
+
try {
|
|
78
|
+
// Remove ANSI color codes for file logging
|
|
79
|
+
const cleanMessage = message.replace(/\u001b\[[0-9;]*m/g, '');
|
|
80
|
+
|
|
81
|
+
// Append to test log file
|
|
82
|
+
fs.appendFileSync(this.currentTestLogFile, cleanMessage + '\n');
|
|
83
|
+
} catch (error) {
|
|
84
|
+
// Silently fail to avoid disrupting the test run
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private logJson(data: any) {
|
|
89
|
+
const jsonString = JSON.stringify(data);
|
|
90
|
+
console.log(jsonString);
|
|
91
|
+
|
|
92
|
+
// Also log to test file if --logfile is specified
|
|
93
|
+
if (this.currentTestLogFile) {
|
|
94
|
+
this.logToTestFile(jsonString);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Section separators
|
|
99
|
+
sectionStart(title: string) {
|
|
100
|
+
if (this.options.quiet || this.options.json) return;
|
|
101
|
+
this.log(this.format(`\nāāā ${title} āāā`, 'cyan'));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
sectionEnd() {
|
|
105
|
+
if (this.options.quiet || this.options.json) return;
|
|
106
|
+
this.log(this.format('ā'.repeat(50), 'dim'));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Progress indication
|
|
110
|
+
progress(current: number, total: number, message: string) {
|
|
111
|
+
if (this.options.quiet || this.options.json) return;
|
|
112
|
+
const percentage = Math.round((current / total) * 100);
|
|
113
|
+
const filled = Math.round((current / total) * 20);
|
|
114
|
+
const empty = 20 - filled;
|
|
115
|
+
|
|
116
|
+
this.log(this.format(`\nš Progress: ${current}/${total} (${percentage}%)`, 'cyan'));
|
|
117
|
+
this.log(this.format(`[${'ā'.repeat(filled)}${'ā'.repeat(empty)}] ${message}`, 'dim'));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Test discovery
|
|
121
|
+
testDiscovery(count: number, pattern: string, executionMode: string) {
|
|
122
|
+
if (this.options.json) {
|
|
123
|
+
this.logJson({ event: 'discovery', count, pattern, executionMode });
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (this.options.quiet) {
|
|
128
|
+
this.log(`Found ${count} tests`);
|
|
129
|
+
} else {
|
|
130
|
+
this.log(this.format(`\nš Test Discovery`, 'bold'));
|
|
131
|
+
this.log(this.format(` Mode: ${executionMode}`, 'dim'));
|
|
132
|
+
this.log(this.format(` Pattern: ${pattern}`, 'dim'));
|
|
133
|
+
this.log(this.format(` Found: ${count} test file(s)`, 'green'));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Test execution
|
|
138
|
+
testFileStart(filename: string, runtime: string, index: number, total: number) {
|
|
139
|
+
this.currentFileResult = {
|
|
140
|
+
file: filename,
|
|
141
|
+
passed: 0,
|
|
142
|
+
failed: 0,
|
|
143
|
+
total: 0,
|
|
144
|
+
duration: 0,
|
|
145
|
+
tests: []
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// Only set up test log file if --logfile option is specified
|
|
149
|
+
if (this.options.logFile) {
|
|
150
|
+
const baseFilename = path.basename(filename, '.ts');
|
|
151
|
+
this.currentTestLogFile = path.join('.nogit', 'testlogs', `${baseFilename}.log`);
|
|
152
|
+
|
|
153
|
+
// Ensure the directory exists
|
|
154
|
+
const logDir = path.dirname(this.currentTestLogFile);
|
|
155
|
+
if (!fs.existsSync(logDir)) {
|
|
156
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Clear the log file for this test
|
|
160
|
+
fs.writeFileSync(this.currentTestLogFile, '');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (this.options.json) {
|
|
164
|
+
this.logJson({ event: 'fileStart', filename, runtime, index, total });
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (this.options.quiet) return;
|
|
169
|
+
|
|
170
|
+
this.log(this.format(`\nā¶ļø ${filename} (${index}/${total})`, 'blue'));
|
|
171
|
+
this.log(this.format(` Runtime: ${runtime}`, 'dim'));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
testResult(testName: string, passed: boolean, duration: number, error?: string) {
|
|
175
|
+
if (this.currentFileResult) {
|
|
176
|
+
this.currentFileResult.tests.push({ name: testName, passed, duration, error });
|
|
177
|
+
this.currentFileResult.total++;
|
|
178
|
+
if (passed) {
|
|
179
|
+
this.currentFileResult.passed++;
|
|
180
|
+
} else {
|
|
181
|
+
this.currentFileResult.failed++;
|
|
182
|
+
}
|
|
183
|
+
this.currentFileResult.duration += duration;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (this.options.json) {
|
|
187
|
+
this.logJson({ event: 'testResult', testName, passed, duration, error });
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const icon = passed ? 'ā
' : 'ā';
|
|
192
|
+
const color = passed ? 'green' : 'red';
|
|
193
|
+
|
|
194
|
+
if (this.options.quiet) {
|
|
195
|
+
this.log(`${icon} ${testName}`);
|
|
196
|
+
} else {
|
|
197
|
+
this.log(this.format(` ${icon} ${testName} (${duration}ms)`, color));
|
|
198
|
+
if (error && !passed) {
|
|
199
|
+
this.log(this.format(` ${error}`, 'red'));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
testFileEnd(passed: number, failed: number, duration: number) {
|
|
205
|
+
if (this.currentFileResult) {
|
|
206
|
+
this.fileResults.push(this.currentFileResult);
|
|
207
|
+
this.currentFileResult = null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (this.options.json) {
|
|
211
|
+
this.logJson({ event: 'fileEnd', passed, failed, duration });
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (!this.options.quiet) {
|
|
216
|
+
const total = passed + failed;
|
|
217
|
+
const status = failed === 0 ? 'PASSED' : 'FAILED';
|
|
218
|
+
const color = failed === 0 ? 'green' : 'red';
|
|
219
|
+
this.log(this.format(` Summary: ${passed}/${total} ${status}`, color));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Clear the current test log file reference only if using --logfile
|
|
223
|
+
if (this.options.logFile) {
|
|
224
|
+
this.currentTestLogFile = null;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// TAP output forwarding (for TAP protocol messages)
|
|
229
|
+
tapOutput(message: string, isError: boolean = false) {
|
|
230
|
+
if (this.options.json) return;
|
|
231
|
+
|
|
232
|
+
// Never show raw TAP protocol messages in console
|
|
233
|
+
// They are already processed by TapParser and shown in our format
|
|
234
|
+
|
|
235
|
+
// Always log to test file if --logfile is specified
|
|
236
|
+
if (this.currentTestLogFile) {
|
|
237
|
+
this.logToTestFile(` ${message}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Console output from test files (non-TAP output)
|
|
242
|
+
testConsoleOutput(message: string) {
|
|
243
|
+
if (this.options.json) return;
|
|
244
|
+
|
|
245
|
+
// Show console output from test files only in verbose mode
|
|
246
|
+
if (this.options.verbose) {
|
|
247
|
+
this.log(this.format(` ${message}`, 'dim'));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Always log to test file if --logfile is specified
|
|
251
|
+
if (this.currentTestLogFile) {
|
|
252
|
+
this.logToTestFile(` ${message}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Browser console
|
|
257
|
+
browserConsole(message: string, level: string = 'log') {
|
|
258
|
+
if (this.options.json) {
|
|
259
|
+
this.logJson({ event: 'browserConsole', message, level });
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (!this.options.quiet) {
|
|
264
|
+
const prefix = level === 'error' ? 'šā' : 'š';
|
|
265
|
+
const color = level === 'error' ? 'red' : 'magenta';
|
|
266
|
+
this.log(this.format(` ${prefix} ${message}`, color));
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Final summary
|
|
271
|
+
summary() {
|
|
272
|
+
const totalDuration = Date.now() - this.startTime;
|
|
273
|
+
const summary: TestSummary = {
|
|
274
|
+
totalFiles: this.fileResults.length,
|
|
275
|
+
totalTests: this.fileResults.reduce((sum, r) => sum + r.total, 0),
|
|
276
|
+
totalPassed: this.fileResults.reduce((sum, r) => sum + r.passed, 0),
|
|
277
|
+
totalFailed: this.fileResults.reduce((sum, r) => sum + r.failed, 0),
|
|
278
|
+
totalDuration,
|
|
279
|
+
fileResults: this.fileResults
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
if (this.options.json) {
|
|
283
|
+
this.logJson({ event: 'summary', summary });
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (this.options.quiet) {
|
|
288
|
+
const status = summary.totalFailed === 0 ? 'PASSED' : 'FAILED';
|
|
289
|
+
this.log(`\nSummary: ${summary.totalPassed}/${summary.totalTests} | ${totalDuration}ms | ${status}`);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Detailed summary
|
|
294
|
+
this.log(this.format('\nš Test Summary', 'bold'));
|
|
295
|
+
this.log(this.format('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā', 'dim'));
|
|
296
|
+
this.log(this.format(`ā Total Files: ${summary.totalFiles.toString().padStart(14)} ā`, 'white'));
|
|
297
|
+
this.log(this.format(`ā Total Tests: ${summary.totalTests.toString().padStart(14)} ā`, 'white'));
|
|
298
|
+
this.log(this.format(`ā Passed: ${summary.totalPassed.toString().padStart(14)} ā`, 'green'));
|
|
299
|
+
this.log(this.format(`ā Failed: ${summary.totalFailed.toString().padStart(14)} ā`, summary.totalFailed > 0 ? 'red' : 'green'));
|
|
300
|
+
this.log(this.format(`ā Duration: ${totalDuration.toString().padStart(14)}ms ā`, 'white'));
|
|
301
|
+
this.log(this.format('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā', 'dim'));
|
|
302
|
+
|
|
303
|
+
// File results
|
|
304
|
+
if (summary.totalFailed > 0) {
|
|
305
|
+
this.log(this.format('\nā Failed Tests:', 'red'));
|
|
306
|
+
this.fileResults.forEach(fileResult => {
|
|
307
|
+
if (fileResult.failed > 0) {
|
|
308
|
+
this.log(this.format(`\n ${fileResult.file}`, 'yellow'));
|
|
309
|
+
fileResult.tests.filter(t => !t.passed).forEach(test => {
|
|
310
|
+
this.log(this.format(` ā ${test.name}`, 'red'));
|
|
311
|
+
if (test.error) {
|
|
312
|
+
this.log(this.format(` ${test.error}`, 'dim'));
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Performance metrics
|
|
320
|
+
if (this.options.verbose) {
|
|
321
|
+
const avgDuration = Math.round(totalDuration / summary.totalTests);
|
|
322
|
+
const slowestTest = this.fileResults
|
|
323
|
+
.flatMap(r => r.tests)
|
|
324
|
+
.sort((a, b) => b.duration - a.duration)[0];
|
|
325
|
+
|
|
326
|
+
this.log(this.format('\nā±ļø Performance Metrics:', 'cyan'));
|
|
327
|
+
this.log(this.format(` Average per test: ${avgDuration}ms`, 'white'));
|
|
328
|
+
if (slowestTest) {
|
|
329
|
+
this.log(this.format(` Slowest test: ${slowestTest.name} (${slowestTest.duration}ms)`, 'yellow'));
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Final status
|
|
334
|
+
const status = summary.totalFailed === 0 ? 'ALL TESTS PASSED! š' : 'SOME TESTS FAILED! ā';
|
|
335
|
+
const statusColor = summary.totalFailed === 0 ? 'green' : 'red';
|
|
336
|
+
this.log(this.format(`\n${status}`, statusColor));
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Error display
|
|
340
|
+
error(message: string, file?: string, stack?: string) {
|
|
341
|
+
if (this.options.json) {
|
|
342
|
+
this.logJson({ event: 'error', message, file, stack });
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (this.options.quiet) {
|
|
347
|
+
console.error(`ERROR: ${message}`);
|
|
348
|
+
} else {
|
|
349
|
+
this.log(this.format('\nā ļø Error', 'red'));
|
|
350
|
+
if (file) this.log(this.format(` File: ${file}`, 'yellow'));
|
|
351
|
+
this.log(this.format(` ${message}`, 'red'));
|
|
352
|
+
if (stack && this.options.verbose) {
|
|
353
|
+
this.log(this.format(` Stack:`, 'dim'));
|
|
354
|
+
this.log(this.format(stack.split('\n').map(line => ` ${line}`).join('\n'), 'dim'));
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
package/readme.plan.md
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
# Plan for adding single file and glob pattern execution support to tstest
|
|
2
|
-
|
|
3
|
-
!! FIRST: Reread /home/philkunz/.claude/CLAUDE.md to ensure following all guidelines !!
|
|
4
|
-
|
|
5
|
-
## Goal - ā
COMPLETED
|
|
6
|
-
- ā
Make `tstest test/test.abc.ts` run the specified file directly
|
|
7
|
-
- ā
Support glob patterns like `tstest test/*.spec.ts` or `tstest test/**/*.test.ts`
|
|
8
|
-
- ā
Maintain backward compatibility with directory argument
|
|
9
|
-
|
|
10
|
-
## Current behavior - UPDATED
|
|
11
|
-
- ā
tstest now supports three modes: directory, single file, and glob patterns
|
|
12
|
-
- ā
Directory mode now searches recursively using `**/test*.ts` pattern
|
|
13
|
-
- ā
Single file mode runs a specific test file
|
|
14
|
-
- ā
Glob mode runs files matching the pattern
|
|
15
|
-
|
|
16
|
-
## Completed changes
|
|
17
|
-
|
|
18
|
-
### 1. ā
Update cli argument handling in index.ts
|
|
19
|
-
- ā
Detect argument type: file path, glob pattern, or directory
|
|
20
|
-
- ā
Check if argument contains glob characters (*, **, ?, [], etc.)
|
|
21
|
-
- ā
Pass appropriate mode to TsTest constructor
|
|
22
|
-
- ā
Added TestExecutionMode enum
|
|
23
|
-
|
|
24
|
-
### 2. ā
Modify TsTest constructor and class
|
|
25
|
-
- ā
Add support for three modes: directory, file, glob
|
|
26
|
-
- ā
Update constructor to accept pattern/path and mode
|
|
27
|
-
- ā
Added executionMode property to track the mode
|
|
28
|
-
|
|
29
|
-
### 3. ā
Update TestDirectory class
|
|
30
|
-
- ā
Used `listFileTree` for glob pattern support
|
|
31
|
-
- ā
Used `SmartFile.fromFilePath` for single file loading
|
|
32
|
-
- ā
Refactored to support all three modes in `_init` method
|
|
33
|
-
- ā
Return appropriate file array based on mode
|
|
34
|
-
- ā
Changed default directory behavior to recursive search
|
|
35
|
-
- ā
When directory argument: use `**/test*.ts` pattern for recursive search
|
|
36
|
-
- ā
This ensures subdirectories are included in test discovery
|
|
37
|
-
|
|
38
|
-
### 4. ā
Test the implementation
|
|
39
|
-
- ā
Created test file `test/test.single.ts` for single file functionality
|
|
40
|
-
- ā
Created test file `test/test.glob.ts` for glob pattern functionality
|
|
41
|
-
- ā
Created test in subdirectory `test/subdir/test.sub.ts` for recursive search
|
|
42
|
-
- ā
Tested with existing test files for backward compatibility
|
|
43
|
-
- ā
Tested glob patterns: `test/test.*.ts` works correctly
|
|
44
|
-
- ā
Verified that default behavior now includes subdirectories
|
|
45
|
-
|
|
46
|
-
## Implementation completed
|
|
47
|
-
1. ā
CLI argument type detection implemented
|
|
48
|
-
2. ā
TsTest class supports all three modes
|
|
49
|
-
3. ā
TestDirectory handles files, globs, and directories
|
|
50
|
-
4. ā
Default pattern changed from `test*.ts` to `**/test*.ts` for recursive search
|
|
51
|
-
5. ā
Comprehensive tests added and all modes verified
|