@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.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/index.js +43 -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 +42 -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 +48 -0
- package/dist_ts/tstest.logging.js +228 -0
- package/package.json +1 -1
- package/readme.plan.md +196 -48
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/index.ts +46 -3
- package/ts/tstest.classes.tap.combinator.ts +19 -41
- package/ts/tstest.classes.tap.parser.ts +44 -47
- package/ts/tstest.classes.tstest.ts +38 -35
- package/ts/tstest.logging.ts +285 -0
|
@@ -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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
let failGlobal = false;
|
|
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
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
58
|
-
if
|
|
59
|
-
|
|
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
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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
|
-
|
|
77
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
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
|
-
|
|
176
|
-
`${
|
|
177
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
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
|
-
|
|
202
|
-
`${
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
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,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
|
+
}
|