@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.
Files changed (37) hide show
  1. package/dist/assertions-BImuwZsB.d.mts +458 -0
  2. package/dist/assertions-BImuwZsB.d.ts +458 -0
  3. package/dist/assertions.d.mts +1 -0
  4. package/dist/assertions.d.ts +1 -0
  5. package/dist/assertions.js +625 -0
  6. package/dist/assertions.js.map +1 -0
  7. package/dist/assertions.mjs +621 -0
  8. package/dist/assertions.mjs.map +1 -0
  9. package/dist/index.d.mts +246 -0
  10. package/dist/index.d.ts +246 -0
  11. package/dist/index.js +1465 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/index.mjs +1435 -0
  14. package/dist/index.mjs.map +1 -0
  15. package/package.json +121 -0
  16. package/src/assertions.test.ts +490 -0
  17. package/src/assertions.ts +779 -0
  18. package/src/index.test.ts +490 -0
  19. package/src/index.ts +601 -0
  20. package/src/jsdom.d.ts +7 -0
  21. package/src/multilingual/README.md +295 -0
  22. package/src/multilingual/bundle-builder.ts +345 -0
  23. package/src/multilingual/cli.ts +224 -0
  24. package/src/multilingual/index.ts +68 -0
  25. package/src/multilingual/orchestrator.ts +275 -0
  26. package/src/multilingual/pattern-loader.ts +176 -0
  27. package/src/multilingual/reporters/console-reporter.ts +294 -0
  28. package/src/multilingual/reporters/json-reporter.ts +99 -0
  29. package/src/multilingual/reporters/regression-reporter.ts +263 -0
  30. package/src/multilingual/tools/analyze-failures.ts +326 -0
  31. package/src/multilingual/types.ts +268 -0
  32. package/src/multilingual/validators/parse-diagnostics.ts +337 -0
  33. package/src/multilingual/validators/parse-validator.ts +193 -0
  34. package/src/multilingual/validators/size-validator.ts +187 -0
  35. package/src/runner.test.ts +813 -0
  36. package/src/runner.ts +639 -0
  37. 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
+ }