@hyperfixi/testing-framework 2.0.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/assertions-BImuwZsB.d.mts +458 -0
- package/dist/assertions-BImuwZsB.d.ts +458 -0
- package/dist/assertions.d.mts +1 -0
- package/dist/assertions.d.ts +1 -0
- package/dist/assertions.js +625 -0
- package/dist/assertions.js.map +1 -0
- package/dist/assertions.mjs +621 -0
- package/dist/assertions.mjs.map +1 -0
- package/dist/index.d.mts +246 -0
- package/dist/index.d.ts +246 -0
- package/dist/index.js +1465 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1435 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +121 -0
- package/src/assertions.test.ts +490 -0
- package/src/assertions.ts +779 -0
- package/src/index.test.ts +490 -0
- package/src/index.ts +601 -0
- package/src/jsdom.d.ts +7 -0
- package/src/multilingual/README.md +295 -0
- package/src/multilingual/bundle-builder.ts +345 -0
- package/src/multilingual/cli.ts +224 -0
- package/src/multilingual/index.ts +68 -0
- package/src/multilingual/orchestrator.ts +275 -0
- package/src/multilingual/pattern-loader.ts +176 -0
- package/src/multilingual/reporters/console-reporter.ts +294 -0
- package/src/multilingual/reporters/json-reporter.ts +99 -0
- package/src/multilingual/reporters/regression-reporter.ts +263 -0
- package/src/multilingual/tools/analyze-failures.ts +326 -0
- package/src/multilingual/types.ts +268 -0
- package/src/multilingual/validators/parse-diagnostics.ts +337 -0
- package/src/multilingual/validators/parse-validator.ts +193 -0
- package/src/multilingual/validators/size-validator.ts +187 -0
- package/src/runner.test.ts +813 -0
- package/src/runner.ts +639 -0
- package/src/types.ts +505 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pattern Loader - Queries patterns database for test cases
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
getAllPatterns,
|
|
7
|
+
getTranslationsByLanguage,
|
|
8
|
+
getVerifiedTranslations,
|
|
9
|
+
getHighConfidenceTranslations,
|
|
10
|
+
getPatternStats,
|
|
11
|
+
type Pattern,
|
|
12
|
+
type Translation,
|
|
13
|
+
} from '@hyperfixi/patterns-reference';
|
|
14
|
+
import type { LanguageCode, PatternTranslation, TestConfig, SamplingStrategy } from './types';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Load patterns for testing based on configuration
|
|
18
|
+
*/
|
|
19
|
+
export async function loadPatterns(config: TestConfig): Promise<PatternTranslation[]> {
|
|
20
|
+
const languages = config.languages || (await getAllLanguages());
|
|
21
|
+
const results: PatternTranslation[] = [];
|
|
22
|
+
|
|
23
|
+
for (const language of languages) {
|
|
24
|
+
const translations = await loadTranslationsForLanguage(language, config);
|
|
25
|
+
results.push(...translations);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Apply sampling if in quick mode
|
|
29
|
+
if (config.mode === 'quick') {
|
|
30
|
+
const limit = config.quickModeLimit || 10;
|
|
31
|
+
return samplePatterns(results, { type: 'stratified', perCategory: limit });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return results;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Load translations for a specific language
|
|
39
|
+
*/
|
|
40
|
+
async function loadTranslationsForLanguage(
|
|
41
|
+
language: LanguageCode,
|
|
42
|
+
config: TestConfig
|
|
43
|
+
): Promise<PatternTranslation[]> {
|
|
44
|
+
let translations: Translation[];
|
|
45
|
+
|
|
46
|
+
// Query based on configuration
|
|
47
|
+
if (config.verifiedOnly) {
|
|
48
|
+
translations = await getVerifiedTranslations(language, 1000);
|
|
49
|
+
} else if (config.confidenceThreshold !== undefined) {
|
|
50
|
+
// Get high confidence for all languages, then filter
|
|
51
|
+
const allTranslations = await getHighConfidenceTranslations(config.confidenceThreshold, 1000);
|
|
52
|
+
translations = allTranslations.filter(t => t.language === language);
|
|
53
|
+
} else {
|
|
54
|
+
translations = await getTranslationsByLanguage(language, 1000);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Convert to PatternTranslation format
|
|
58
|
+
return translations.map(t => mapToPatternTranslation(t));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Map Translation to PatternTranslation
|
|
63
|
+
*/
|
|
64
|
+
function mapToPatternTranslation(translation: Translation): PatternTranslation {
|
|
65
|
+
return {
|
|
66
|
+
codeExampleId: translation.codeExampleId,
|
|
67
|
+
language: translation.language as LanguageCode,
|
|
68
|
+
hyperscript: translation.hyperscript,
|
|
69
|
+
wordOrder: translation.wordOrder,
|
|
70
|
+
confidence: translation.confidence,
|
|
71
|
+
verifiedParses: translation.verifiedParses,
|
|
72
|
+
roleAlignmentScore: ((translation as any).roleAlignmentScore ?? 0) as number,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get all supported languages from database
|
|
78
|
+
*/
|
|
79
|
+
async function getAllLanguages(): Promise<LanguageCode[]> {
|
|
80
|
+
const stats = await getPatternStats();
|
|
81
|
+
return Object.keys(stats.byLanguage) as LanguageCode[];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Sample patterns based on strategy
|
|
86
|
+
*/
|
|
87
|
+
function samplePatterns(
|
|
88
|
+
patterns: PatternTranslation[],
|
|
89
|
+
strategy: SamplingStrategy
|
|
90
|
+
): PatternTranslation[] {
|
|
91
|
+
switch (strategy.type) {
|
|
92
|
+
case 'all':
|
|
93
|
+
return patterns;
|
|
94
|
+
|
|
95
|
+
case 'first':
|
|
96
|
+
return patterns.slice(0, strategy.count);
|
|
97
|
+
|
|
98
|
+
case 'random':
|
|
99
|
+
return shuffleArray([...patterns]).slice(0, strategy.count);
|
|
100
|
+
|
|
101
|
+
case 'stratified': {
|
|
102
|
+
// Group by language and sample from each
|
|
103
|
+
const byLanguage = new Map<LanguageCode, PatternTranslation[]>();
|
|
104
|
+
for (const pattern of patterns) {
|
|
105
|
+
const lang = pattern.language;
|
|
106
|
+
if (!byLanguage.has(lang)) {
|
|
107
|
+
byLanguage.set(lang, []);
|
|
108
|
+
}
|
|
109
|
+
byLanguage.get(lang)!.push(pattern);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const sampled: PatternTranslation[] = [];
|
|
113
|
+
for (const langPatterns of byLanguage.values()) {
|
|
114
|
+
sampled.push(...langPatterns.slice(0, strategy.perCategory));
|
|
115
|
+
}
|
|
116
|
+
return sampled;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
default:
|
|
120
|
+
return patterns;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Fisher-Yates shuffle
|
|
126
|
+
*/
|
|
127
|
+
function shuffleArray<T>(array: T[]): T[] {
|
|
128
|
+
for (let i = array.length - 1; i > 0; i--) {
|
|
129
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
130
|
+
const temp = array[i];
|
|
131
|
+
array[i] = array[j] as T;
|
|
132
|
+
array[j] = temp as T;
|
|
133
|
+
}
|
|
134
|
+
return array;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Get pattern statistics for reporting
|
|
139
|
+
*/
|
|
140
|
+
export async function getPatternStatistics() {
|
|
141
|
+
return await getPatternStats();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Load patterns by category
|
|
146
|
+
*/
|
|
147
|
+
export async function loadPatternsByCategory(
|
|
148
|
+
categories: string[],
|
|
149
|
+
languages: LanguageCode[]
|
|
150
|
+
): Promise<PatternTranslation[]> {
|
|
151
|
+
const results: PatternTranslation[] = [];
|
|
152
|
+
|
|
153
|
+
for (const language of languages) {
|
|
154
|
+
const translations = await getTranslationsByLanguage(language, 1000);
|
|
155
|
+
|
|
156
|
+
// Filter by category (would need to join with patterns table in real implementation)
|
|
157
|
+
// For now, return all translations
|
|
158
|
+
results.push(...translations.map(t => mapToPatternTranslation(t)));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return results;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Count patterns by language
|
|
166
|
+
*/
|
|
167
|
+
export async function countPatternsByLanguage(): Promise<Record<LanguageCode, number>> {
|
|
168
|
+
const stats = await getPatternStats();
|
|
169
|
+
const counts: Record<string, number> = {};
|
|
170
|
+
|
|
171
|
+
for (const [lang, data] of Object.entries(stats.byLanguage)) {
|
|
172
|
+
counts[lang] = data.count;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return counts as Record<LanguageCode, number>;
|
|
176
|
+
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Console Reporter - Minimal, helpful console output
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
Reporter,
|
|
7
|
+
TestConfig,
|
|
8
|
+
LanguageCode,
|
|
9
|
+
LanguageResults,
|
|
10
|
+
TestResults,
|
|
11
|
+
RegressionResult,
|
|
12
|
+
} from '../types';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* ANSI color codes for terminal output
|
|
16
|
+
*/
|
|
17
|
+
const colors = {
|
|
18
|
+
reset: '\x1b[0m',
|
|
19
|
+
bright: '\x1b[1m',
|
|
20
|
+
dim: '\x1b[2m',
|
|
21
|
+
|
|
22
|
+
green: '\x1b[32m',
|
|
23
|
+
yellow: '\x1b[33m',
|
|
24
|
+
red: '\x1b[31m',
|
|
25
|
+
cyan: '\x1b[36m',
|
|
26
|
+
gray: '\x1b[90m',
|
|
27
|
+
|
|
28
|
+
bgGreen: '\x1b[42m',
|
|
29
|
+
bgYellow: '\x1b[43m',
|
|
30
|
+
bgRed: '\x1b[41m',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Console Reporter
|
|
35
|
+
*
|
|
36
|
+
* Outputs minimal, actionable test results to the console.
|
|
37
|
+
*/
|
|
38
|
+
export class ConsoleReporter implements Reporter {
|
|
39
|
+
private verbose: boolean;
|
|
40
|
+
private useColors: boolean;
|
|
41
|
+
|
|
42
|
+
constructor(options: { verbose?: boolean; useColors?: boolean } = {}) {
|
|
43
|
+
this.verbose = options.verbose ?? false;
|
|
44
|
+
this.useColors = options.useColors ?? true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Report test start
|
|
49
|
+
*/
|
|
50
|
+
reportStart(config: TestConfig): void {
|
|
51
|
+
this.log('\n' + this.header('Multilingual Test Runner v1.0.0'));
|
|
52
|
+
this.log(this.separator());
|
|
53
|
+
this.log('');
|
|
54
|
+
|
|
55
|
+
// Configuration summary
|
|
56
|
+
const languages = config.languages?.join(', ') || 'all';
|
|
57
|
+
const mode =
|
|
58
|
+
config.mode === 'quick'
|
|
59
|
+
? `quick mode (${config.quickModeLimit || 10} patterns/lang)`
|
|
60
|
+
: 'full mode';
|
|
61
|
+
|
|
62
|
+
this.log(`${this.dim('Languages:')} ${languages} (${config.languages?.length || 'all'})`);
|
|
63
|
+
if (config.bundle) {
|
|
64
|
+
this.log(`${this.dim('Bundle:')} ${config.bundle}`);
|
|
65
|
+
}
|
|
66
|
+
this.log(`${this.dim('Mode:')} ${mode}`);
|
|
67
|
+
if (config.confidenceThreshold) {
|
|
68
|
+
this.log(`${this.dim('Min Confidence:')} ${config.confidenceThreshold}`);
|
|
69
|
+
}
|
|
70
|
+
this.log('');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Report language test start
|
|
75
|
+
*/
|
|
76
|
+
reportLanguageStart(language: LanguageCode, bundle: string): void {
|
|
77
|
+
if (this.verbose) {
|
|
78
|
+
this.log(
|
|
79
|
+
`${this.cyan('→')} Testing ${this.bright(language.toUpperCase())} with ${bundle}...`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Report language test complete
|
|
86
|
+
*/
|
|
87
|
+
reportLanguageComplete(results: LanguageResults): void {
|
|
88
|
+
const status =
|
|
89
|
+
results.status === 'pass'
|
|
90
|
+
? this.green('✓')
|
|
91
|
+
: results.status === 'warning'
|
|
92
|
+
? this.yellow('⚠')
|
|
93
|
+
: this.red('✗');
|
|
94
|
+
|
|
95
|
+
const rate = `${results.parseSuccess}/${results.parseSuccess + results.parseFailure}`;
|
|
96
|
+
const percentage = `(${(results.parseRate * 100).toFixed(0)}%)`;
|
|
97
|
+
const duration = `${(results.duration / 1000).toFixed(1)}s`;
|
|
98
|
+
const confidence = results.avgConfidence > 0 ? `conf: ${results.avgConfidence.toFixed(2)}` : '';
|
|
99
|
+
|
|
100
|
+
this.log(
|
|
101
|
+
` ${status} ${this.bright(results.language.toUpperCase())}: ` +
|
|
102
|
+
`${rate} ${percentage} ` +
|
|
103
|
+
`${this.dim('⏱')} ${duration}` +
|
|
104
|
+
(confidence ? ` ${this.dim(confidence)}` : '')
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
// Show failures if verbose
|
|
108
|
+
if (this.verbose && results.parseFailure > 0) {
|
|
109
|
+
const failures = results.parseResults.filter(r => !r.success);
|
|
110
|
+
this.log(` ${this.dim(`Failed patterns (${failures.length}):`)}`);
|
|
111
|
+
|
|
112
|
+
// Show all failures with pattern ID for tracking
|
|
113
|
+
for (const failure of failures) {
|
|
114
|
+
this.log(` ${this.dim('✗')} ${failure.pattern.codeExampleId || 'unknown'}`);
|
|
115
|
+
this.log(` ${this.dim('Input:')} ${failure.pattern.hyperscript}`);
|
|
116
|
+
if (failure.error) {
|
|
117
|
+
this.log(` ${this.red('Error:')} ${failure.error}`);
|
|
118
|
+
}
|
|
119
|
+
if (failure.confidence !== undefined) {
|
|
120
|
+
this.log(` ${this.dim('Confidence:')} ${failure.confidence.toFixed(2)}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Report complete test run
|
|
128
|
+
*/
|
|
129
|
+
reportComplete(results: TestResults): void {
|
|
130
|
+
this.log('');
|
|
131
|
+
this.log(this.separator());
|
|
132
|
+
|
|
133
|
+
const statusIcon =
|
|
134
|
+
results.summary.overallStatus === 'pass'
|
|
135
|
+
? this.green('✓ PASS')
|
|
136
|
+
: results.summary.overallStatus === 'warning'
|
|
137
|
+
? this.yellow('⚠ WARNING')
|
|
138
|
+
: this.red('✗ FAIL');
|
|
139
|
+
|
|
140
|
+
const summary = `${results.summary.totalSuccess}/${results.summary.totalPatterns}`;
|
|
141
|
+
const duration = `${(results.summary.totalDuration / 1000).toFixed(1)}s`;
|
|
142
|
+
|
|
143
|
+
this.log(
|
|
144
|
+
`${this.bright('Summary:')} ${statusIcon} ` +
|
|
145
|
+
`${summary} ` +
|
|
146
|
+
`${this.dim('Duration:')} ${duration}`
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
// Show bundle sizes if available
|
|
150
|
+
if (Object.keys(results.bundles).length > 0) {
|
|
151
|
+
this.log('');
|
|
152
|
+
this.log(this.dim('Bundle Sizes:'));
|
|
153
|
+
for (const [bundle, info] of Object.entries(results.bundles)) {
|
|
154
|
+
const size = this.formatSize(info.size);
|
|
155
|
+
const gzipSize = info.gzipSize ? ` (${this.formatSize(info.gzipSize)} gzipped)` : '';
|
|
156
|
+
this.log(` ${bundle}: ${size}${gzipSize}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
this.log('');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Report regression comparison
|
|
165
|
+
*/
|
|
166
|
+
reportRegression(results: RegressionResult[]): void {
|
|
167
|
+
this.log('');
|
|
168
|
+
this.log(this.header('Regression Analysis'));
|
|
169
|
+
this.log(this.separator());
|
|
170
|
+
|
|
171
|
+
for (const result of results) {
|
|
172
|
+
const statusIcon =
|
|
173
|
+
result.status === 'improved'
|
|
174
|
+
? this.green('↑')
|
|
175
|
+
: result.status === 'regressed'
|
|
176
|
+
? this.red('↓')
|
|
177
|
+
: this.gray('→');
|
|
178
|
+
|
|
179
|
+
this.log(`${statusIcon} ${this.bright(result.language.toUpperCase())}`);
|
|
180
|
+
|
|
181
|
+
// Parse rate delta
|
|
182
|
+
if (Math.abs(result.parseRateDelta) > 0.01) {
|
|
183
|
+
const delta =
|
|
184
|
+
result.parseRateDelta > 0
|
|
185
|
+
? this.green(`+${result.parseRateDelta.toFixed(1)}%`)
|
|
186
|
+
: this.red(`${result.parseRateDelta.toFixed(1)}%`);
|
|
187
|
+
this.log(` Parse Rate: ${delta}`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Confidence delta
|
|
191
|
+
if (Math.abs(result.avgConfidenceDelta) > 0.01) {
|
|
192
|
+
const delta =
|
|
193
|
+
result.avgConfidenceDelta > 0
|
|
194
|
+
? this.green(`+${result.avgConfidenceDelta.toFixed(2)}`)
|
|
195
|
+
: this.red(`${result.avgConfidenceDelta.toFixed(2)}`);
|
|
196
|
+
this.log(` Avg Confidence: ${delta}`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// New failures
|
|
200
|
+
if (result.newFailures.length > 0) {
|
|
201
|
+
this.log(` ${this.red('New Failures:')} ${result.newFailures.length}`);
|
|
202
|
+
if (this.verbose) {
|
|
203
|
+
result.newFailures.slice(0, 5).forEach(id => {
|
|
204
|
+
this.log(` ${this.dim('- ' + id)}`);
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// New successes
|
|
210
|
+
if (result.newSuccesses.length > 0) {
|
|
211
|
+
this.log(` ${this.green('New Successes:')} ${result.newSuccesses.length}`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
this.log('');
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Report error
|
|
220
|
+
*/
|
|
221
|
+
reportError(error: Error): void {
|
|
222
|
+
this.log('');
|
|
223
|
+
this.log(this.red('✗ Error: ') + error.message);
|
|
224
|
+
if (this.verbose && error.stack) {
|
|
225
|
+
this.log(this.dim(error.stack));
|
|
226
|
+
}
|
|
227
|
+
this.log('');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// =============================================================================
|
|
231
|
+
// Formatting Helpers
|
|
232
|
+
// =============================================================================
|
|
233
|
+
|
|
234
|
+
private log(message: string): void {
|
|
235
|
+
console.log(message);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private separator(char: string = '━', length: number = 60): string {
|
|
239
|
+
return this.dim(char.repeat(length));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private header(text: string): string {
|
|
243
|
+
return this.bright(text);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private formatSize(bytes: number): string {
|
|
247
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
248
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
249
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// =============================================================================
|
|
253
|
+
// Color Helpers
|
|
254
|
+
// =============================================================================
|
|
255
|
+
|
|
256
|
+
private color(text: string, color: string): string {
|
|
257
|
+
return this.useColors ? `${color}${text}${colors.reset}` : text;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
private bright(text: string): string {
|
|
261
|
+
return this.color(text, colors.bright);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private dim(text: string): string {
|
|
265
|
+
return this.color(text, colors.dim);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private green(text: string): string {
|
|
269
|
+
return this.color(text, colors.green);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private yellow(text: string): string {
|
|
273
|
+
return this.color(text, colors.yellow);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private red(text: string): string {
|
|
277
|
+
return this.color(text, colors.red);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private cyan(text: string): string {
|
|
281
|
+
return this.color(text, colors.cyan);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
private gray(text: string): string {
|
|
285
|
+
return this.color(text, colors.gray);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Create a default console reporter
|
|
291
|
+
*/
|
|
292
|
+
export function createConsoleReporter(verbose: boolean = false): ConsoleReporter {
|
|
293
|
+
return new ConsoleReporter({ verbose, useColors: true });
|
|
294
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON Reporter - Structured JSON output for programmatic consumption
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { writeFileSync, mkdirSync } from 'node:fs';
|
|
6
|
+
import { dirname } from 'node:path';
|
|
7
|
+
import type { Reporter, TestConfig, LanguageCode, LanguageResults, TestResults } from '../types';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* JSON Reporter
|
|
11
|
+
*
|
|
12
|
+
* Outputs structured JSON results to a file.
|
|
13
|
+
*/
|
|
14
|
+
export class JSONReporter implements Reporter {
|
|
15
|
+
private outputPath: string;
|
|
16
|
+
private results: TestResults | null = null;
|
|
17
|
+
|
|
18
|
+
constructor(outputPath: string = './test-results/results.json') {
|
|
19
|
+
this.outputPath = outputPath;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Report test start (no-op for JSON reporter)
|
|
24
|
+
*/
|
|
25
|
+
reportStart(_config: TestConfig): void {
|
|
26
|
+
// No output at start
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Report language start (no-op for JSON reporter)
|
|
31
|
+
*/
|
|
32
|
+
reportLanguageStart(_language: LanguageCode, _bundle: string): void {
|
|
33
|
+
// No output during execution
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Report language complete (no-op for JSON reporter)
|
|
38
|
+
*/
|
|
39
|
+
reportLanguageComplete(_results: LanguageResults): void {
|
|
40
|
+
// No output during execution
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Report complete test run - write JSON file
|
|
45
|
+
*/
|
|
46
|
+
reportComplete(results: TestResults): void {
|
|
47
|
+
this.results = results;
|
|
48
|
+
this.writeResults();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Report error
|
|
53
|
+
*/
|
|
54
|
+
reportError(error: Error): void {
|
|
55
|
+
const errorResult = {
|
|
56
|
+
error: {
|
|
57
|
+
message: error.message,
|
|
58
|
+
stack: error.stack,
|
|
59
|
+
timestamp: new Date().toISOString(),
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// Ensure output directory exists
|
|
64
|
+
const dir = dirname(this.outputPath);
|
|
65
|
+
mkdirSync(dir, { recursive: true });
|
|
66
|
+
|
|
67
|
+
writeFileSync(this.outputPath, JSON.stringify(errorResult, null, 2));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Write results to file
|
|
72
|
+
*/
|
|
73
|
+
private writeResults(): void {
|
|
74
|
+
if (!this.results) {
|
|
75
|
+
throw new Error('No results to write');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Ensure output directory exists
|
|
79
|
+
const dir = dirname(this.outputPath);
|
|
80
|
+
mkdirSync(dir, { recursive: true });
|
|
81
|
+
|
|
82
|
+
const json = JSON.stringify(this.results, null, 2);
|
|
83
|
+
writeFileSync(this.outputPath, json);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get results
|
|
88
|
+
*/
|
|
89
|
+
getResults(): TestResults | null {
|
|
90
|
+
return this.results;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Create a JSON reporter
|
|
96
|
+
*/
|
|
97
|
+
export function createJSONReporter(outputPath?: string): JSONReporter {
|
|
98
|
+
return new JSONReporter(outputPath);
|
|
99
|
+
}
|