@sharpee/transcript-tester 0.9.61-beta

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 (137) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/LICENSE +21 -0
  3. package/dist/cli.d.ts +11 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +367 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/condition-evaluator.d.ts +30 -0
  8. package/dist/condition-evaluator.d.ts.map +1 -0
  9. package/dist/condition-evaluator.js +314 -0
  10. package/dist/condition-evaluator.js.map +1 -0
  11. package/dist/fast-cli.d.ts +13 -0
  12. package/dist/fast-cli.d.ts.map +1 -0
  13. package/dist/fast-cli.js +363 -0
  14. package/dist/fast-cli.js.map +1 -0
  15. package/dist/index.d.ts +17 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +48 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/navigator.d.ts +27 -0
  20. package/dist/navigator.d.ts.map +1 -0
  21. package/dist/navigator.js +303 -0
  22. package/dist/navigator.js.map +1 -0
  23. package/dist/parser.d.ts +19 -0
  24. package/dist/parser.d.ts.map +1 -0
  25. package/dist/parser.js +453 -0
  26. package/dist/parser.js.map +1 -0
  27. package/dist/reporter.d.ts +41 -0
  28. package/dist/reporter.d.ts.map +1 -0
  29. package/dist/reporter.js +386 -0
  30. package/dist/reporter.js.map +1 -0
  31. package/dist/runner.d.ts +44 -0
  32. package/dist/runner.d.ts.map +1 -0
  33. package/dist/runner.js +977 -0
  34. package/dist/runner.js.map +1 -0
  35. package/dist/story-loader.d.ts +31 -0
  36. package/dist/story-loader.d.ts.map +1 -0
  37. package/dist/story-loader.js +169 -0
  38. package/dist/story-loader.js.map +1 -0
  39. package/dist/types.d.ts +204 -0
  40. package/dist/types.d.ts.map +1 -0
  41. package/dist/types.js +8 -0
  42. package/dist/types.js.map +1 -0
  43. package/dist-esm/cli.d.ts +11 -0
  44. package/dist-esm/cli.d.ts.map +1 -0
  45. package/dist-esm/cli.js +332 -0
  46. package/dist-esm/cli.js.map +1 -0
  47. package/dist-esm/condition-evaluator.d.ts +30 -0
  48. package/dist-esm/condition-evaluator.d.ts.map +1 -0
  49. package/dist-esm/condition-evaluator.js +311 -0
  50. package/dist-esm/condition-evaluator.js.map +1 -0
  51. package/dist-esm/fast-cli.d.ts +13 -0
  52. package/dist-esm/fast-cli.d.ts.map +1 -0
  53. package/dist-esm/fast-cli.js +328 -0
  54. package/dist-esm/fast-cli.js.map +1 -0
  55. package/dist-esm/index.d.ts +17 -0
  56. package/dist-esm/index.d.ts.map +1 -0
  57. package/dist-esm/index.js +21 -0
  58. package/dist-esm/index.js.map +1 -0
  59. package/dist-esm/navigator.d.ts +27 -0
  60. package/dist-esm/navigator.d.ts.map +1 -0
  61. package/dist-esm/navigator.js +300 -0
  62. package/dist-esm/navigator.js.map +1 -0
  63. package/dist-esm/parser.d.ts +19 -0
  64. package/dist-esm/parser.d.ts.map +1 -0
  65. package/dist-esm/parser.js +415 -0
  66. package/dist-esm/parser.js.map +1 -0
  67. package/dist-esm/reporter.d.ts +41 -0
  68. package/dist-esm/reporter.d.ts.map +1 -0
  69. package/dist-esm/reporter.js +342 -0
  70. package/dist-esm/reporter.js.map +1 -0
  71. package/dist-esm/runner.d.ts +44 -0
  72. package/dist-esm/runner.d.ts.map +1 -0
  73. package/dist-esm/runner.js +941 -0
  74. package/dist-esm/runner.js.map +1 -0
  75. package/dist-esm/story-loader.d.ts +31 -0
  76. package/dist-esm/story-loader.d.ts.map +1 -0
  77. package/dist-esm/story-loader.js +131 -0
  78. package/dist-esm/story-loader.js.map +1 -0
  79. package/dist-esm/types.d.ts +204 -0
  80. package/dist-esm/types.d.ts.map +1 -0
  81. package/dist-esm/types.js +7 -0
  82. package/dist-esm/types.js.map +1 -0
  83. package/dist-npm/cli.d.ts +11 -0
  84. package/dist-npm/cli.d.ts.map +1 -0
  85. package/dist-npm/cli.js +367 -0
  86. package/dist-npm/cli.js.map +1 -0
  87. package/dist-npm/condition-evaluator.d.ts +30 -0
  88. package/dist-npm/condition-evaluator.d.ts.map +1 -0
  89. package/dist-npm/condition-evaluator.js +314 -0
  90. package/dist-npm/condition-evaluator.js.map +1 -0
  91. package/dist-npm/fast-cli.d.ts +13 -0
  92. package/dist-npm/fast-cli.d.ts.map +1 -0
  93. package/dist-npm/fast-cli.js +363 -0
  94. package/dist-npm/fast-cli.js.map +1 -0
  95. package/dist-npm/index.d.ts +17 -0
  96. package/dist-npm/index.d.ts.map +1 -0
  97. package/dist-npm/index.js +48 -0
  98. package/dist-npm/index.js.map +1 -0
  99. package/dist-npm/navigator.d.ts +27 -0
  100. package/dist-npm/navigator.d.ts.map +1 -0
  101. package/dist-npm/navigator.js +303 -0
  102. package/dist-npm/navigator.js.map +1 -0
  103. package/dist-npm/parser.d.ts +19 -0
  104. package/dist-npm/parser.d.ts.map +1 -0
  105. package/dist-npm/parser.js +453 -0
  106. package/dist-npm/parser.js.map +1 -0
  107. package/dist-npm/reporter.d.ts +41 -0
  108. package/dist-npm/reporter.d.ts.map +1 -0
  109. package/dist-npm/reporter.js +386 -0
  110. package/dist-npm/reporter.js.map +1 -0
  111. package/dist-npm/runner.d.ts +44 -0
  112. package/dist-npm/runner.d.ts.map +1 -0
  113. package/dist-npm/runner.js +977 -0
  114. package/dist-npm/runner.js.map +1 -0
  115. package/dist-npm/story-loader.d.ts +31 -0
  116. package/dist-npm/story-loader.d.ts.map +1 -0
  117. package/dist-npm/story-loader.js +169 -0
  118. package/dist-npm/story-loader.js.map +1 -0
  119. package/dist-npm/types.d.ts +204 -0
  120. package/dist-npm/types.d.ts.map +1 -0
  121. package/dist-npm/types.js +8 -0
  122. package/dist-npm/types.js.map +1 -0
  123. package/package.json +49 -0
  124. package/src/cli.ts +385 -0
  125. package/src/condition-evaluator.ts +382 -0
  126. package/src/fast-cli.ts +403 -0
  127. package/src/index.ts +26 -0
  128. package/src/navigator.ts +365 -0
  129. package/src/parser.ts +488 -0
  130. package/src/reporter.ts +409 -0
  131. package/src/runner.ts +1152 -0
  132. package/src/story-loader.ts +168 -0
  133. package/src/types.ts +244 -0
  134. package/tsconfig.esm.json +11 -0
  135. package/tsconfig.esm.tsbuildinfo +1 -0
  136. package/tsconfig.json +22 -0
  137. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,409 @@
1
+ /**
2
+ * Transcript Test Reporter
3
+ *
4
+ * Formats and displays test results with colors and diffs.
5
+ */
6
+
7
+ import * as fs from 'fs';
8
+ import * as path from 'path';
9
+ import {
10
+ TranscriptResult,
11
+ TestRunResult,
12
+ CommandResult,
13
+ AssertionResult
14
+ } from './types';
15
+
16
+ // Use chalk for colors (chalk@4 for CommonJS compatibility)
17
+ import chalk from 'chalk';
18
+
19
+ /**
20
+ * Report options
21
+ */
22
+ export interface ReporterOptions {
23
+ verbose?: boolean;
24
+ showDiff?: boolean;
25
+ color?: boolean;
26
+ }
27
+
28
+ /**
29
+ * Report results of running a single transcript
30
+ */
31
+ export function reportTranscript(
32
+ result: TranscriptResult,
33
+ options: ReporterOptions = {}
34
+ ): void {
35
+ const { verbose = false } = options;
36
+
37
+ console.log();
38
+ console.log(chalk.bold(`Running: ${result.transcript.filePath}`));
39
+
40
+ if (result.transcript.header.title) {
41
+ console.log(chalk.gray(` "${result.transcript.header.title}"`));
42
+ }
43
+
44
+ console.log();
45
+
46
+ for (const cmd of result.commands) {
47
+ reportCommand(cmd, verbose);
48
+ }
49
+
50
+ // Summary line
51
+ console.log();
52
+ reportTranscriptSummary(result);
53
+ }
54
+
55
+ /**
56
+ * Report a single command result
57
+ */
58
+ function reportCommand(result: CommandResult, verbose: boolean): void {
59
+ const { command, passed, expectedFailure, skipped, actualOutput, error } = result;
60
+
61
+ // Command input
62
+ const inputDisplay = chalk.cyan(`> ${command.input}`);
63
+
64
+ // Status indicator
65
+ let status: string;
66
+ if (skipped) {
67
+ status = chalk.yellow('SKIP');
68
+ } else if (expectedFailure) {
69
+ status = chalk.magenta('EXPECTED FAIL');
70
+ } else if (passed) {
71
+ status = chalk.green('PASS');
72
+ } else {
73
+ status = chalk.red('FAIL');
74
+ }
75
+
76
+ // Compact format
77
+ console.log(` ${inputDisplay.padEnd(50)} ${status}`);
78
+
79
+ // Verbose output
80
+ if (verbose || (!passed && !skipped && !expectedFailure)) {
81
+ if (error) {
82
+ console.log(chalk.red(` Error: ${error}`));
83
+ }
84
+
85
+ // Show assertion details
86
+ for (const ar of result.assertionResults) {
87
+ if (!ar.passed || verbose) {
88
+ const icon = ar.passed ? chalk.green('✓') : chalk.red('✗');
89
+ const msg = formatAssertion(ar);
90
+ console.log(` ${icon} ${msg}`);
91
+ }
92
+ }
93
+
94
+ // Always show actual output in verbose mode, or for failures
95
+ if (verbose || (!passed && !skipped)) {
96
+ console.log(chalk.gray(' ─── Output ───'));
97
+ for (const line of actualOutput.split('\n')) {
98
+ if (line.trim()) {
99
+ console.log(chalk.white(` ${line}`));
100
+ }
101
+ }
102
+ console.log(chalk.gray(' ─────────────'));
103
+
104
+ // Show events in verbose mode
105
+ if (verbose && result.actualEvents && result.actualEvents.length > 0) {
106
+ console.log(chalk.gray(` ─── Events (${result.actualEvents.length}) ───`));
107
+ for (const event of result.actualEvents) {
108
+ const dataStr = Object.keys(event.data).length > 0
109
+ ? ` ${chalk.gray(JSON.stringify(event.data))}`
110
+ : '';
111
+ console.log(chalk.blue(` • ${event.type}`) + dataStr);
112
+ }
113
+ console.log(chalk.gray(' ─────────────'));
114
+ }
115
+ }
116
+
117
+ // Show diff for failures with expected output
118
+ if (!passed && !skipped && command.expectedOutput.length > 0) {
119
+ console.log();
120
+ console.log(chalk.gray(' Expected:'));
121
+ for (const line of command.expectedOutput) {
122
+ console.log(chalk.green(` + ${line}`));
123
+ }
124
+ }
125
+ }
126
+
127
+ // Show reason for skips and expected failures
128
+ if (skipped || expectedFailure) {
129
+ const reason = result.assertionResults[0]?.assertion.reason;
130
+ if (reason) {
131
+ console.log(chalk.gray(` (${reason})`));
132
+ }
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Format an assertion result for display
138
+ */
139
+ function formatAssertion(result: AssertionResult): string {
140
+ const { assertion, message } = result;
141
+
142
+ switch (assertion.type) {
143
+ case 'ok':
144
+ return message || 'Exact match';
145
+ case 'ok-contains':
146
+ return message || `Contains "${assertion.value}"`;
147
+ case 'ok-not-contains':
148
+ return message || `Does not contain "${assertion.value}"`;
149
+ case 'ok-matches':
150
+ return message || `Matches ${assertion.pattern}`;
151
+ case 'fail':
152
+ return `Expected failure: ${assertion.reason}`;
153
+ case 'skip':
154
+ return `Skipped: ${assertion.reason || 'no reason given'}`;
155
+ case 'todo':
156
+ return `TODO: ${assertion.reason || 'not implemented'}`;
157
+ case 'event-count':
158
+ return message || `Event count: ${assertion.eventCount}`;
159
+ case 'event-assert': {
160
+ const prefix = assertion.assertTrue ? 'assertTrue' : 'assertFalse';
161
+ const posStr = assertion.eventPosition ? ` Event ${assertion.eventPosition}:` : '';
162
+ const dataStr = assertion.eventData ? ` ${JSON.stringify(assertion.eventData)}` : '';
163
+ return message || `${prefix}:${posStr} ${assertion.eventType}${dataStr}`;
164
+ }
165
+ case 'state-assert': {
166
+ const prefix = assertion.assertTrue ? 'assertTrue' : 'assertFalse';
167
+ return message || `${prefix}: ${assertion.stateExpression}`;
168
+ }
169
+ default:
170
+ return message || 'Unknown assertion';
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Report transcript summary
176
+ */
177
+ function reportTranscriptSummary(result: TranscriptResult): void {
178
+ const { passed, failed, expectedFailures, skipped, duration } = result;
179
+
180
+ const parts: string[] = [];
181
+
182
+ if (passed > 0) {
183
+ parts.push(chalk.green(`${passed} passed`));
184
+ }
185
+ if (failed > 0) {
186
+ parts.push(chalk.red(`${failed} failed`));
187
+ }
188
+ if (expectedFailures > 0) {
189
+ parts.push(chalk.magenta(`${expectedFailures} expected failures`));
190
+ }
191
+ if (skipped > 0) {
192
+ parts.push(chalk.yellow(`${skipped} skipped`));
193
+ }
194
+
195
+ console.log(` ${parts.join(', ')} (${duration}ms)`);
196
+ }
197
+
198
+ /**
199
+ * Report results of running multiple transcripts
200
+ * Note: Individual transcripts should already be reported as they run.
201
+ * This function only shows the aggregate summary.
202
+ */
203
+ export function reportTestRun(result: TestRunResult, options: ReporterOptions = {}): void {
204
+ console.log();
205
+ console.log(chalk.bold('━'.repeat(60)));
206
+ console.log();
207
+
208
+ // Overall summary
209
+ const { totalPassed, totalFailed, totalExpectedFailures, totalSkipped, totalDuration } = result;
210
+ const total = totalPassed + totalFailed + totalExpectedFailures + totalSkipped;
211
+
212
+ console.log(chalk.bold(`Total: ${total} tests in ${result.transcripts.length} transcripts`));
213
+
214
+ const parts: string[] = [];
215
+ if (totalPassed > 0) {
216
+ parts.push(chalk.green(`${totalPassed} passed`));
217
+ }
218
+ if (totalFailed > 0) {
219
+ parts.push(chalk.red(`${totalFailed} failed`));
220
+ }
221
+ if (totalExpectedFailures > 0) {
222
+ parts.push(chalk.magenta(`${totalExpectedFailures} expected failures`));
223
+ }
224
+ if (totalSkipped > 0) {
225
+ parts.push(chalk.yellow(`${totalSkipped} skipped`));
226
+ }
227
+
228
+ console.log(parts.join(', '));
229
+ console.log(chalk.gray(`Duration: ${totalDuration}ms`));
230
+ console.log();
231
+
232
+ // Final status
233
+ if (totalFailed === 0) {
234
+ console.log(chalk.green.bold('✓ All tests passed!'));
235
+ } else {
236
+ console.log(chalk.red.bold(`✗ ${totalFailed} test(s) failed`));
237
+ }
238
+ }
239
+
240
+ /**
241
+ * Get exit code based on results
242
+ */
243
+ export function getExitCode(result: TestRunResult): number {
244
+ if (result.totalFailed > 0) {
245
+ return 1;
246
+ }
247
+ return 0;
248
+ }
249
+
250
+ /**
251
+ * Generate a timestamp string for filenames
252
+ */
253
+ export function generateTimestamp(): string {
254
+ const now = new Date();
255
+ const year = now.getFullYear();
256
+ const month = String(now.getMonth() + 1).padStart(2, '0');
257
+ const day = String(now.getDate()).padStart(2, '0');
258
+ const hours = String(now.getHours()).padStart(2, '0');
259
+ const minutes = String(now.getMinutes()).padStart(2, '0');
260
+ const seconds = String(now.getSeconds()).padStart(2, '0');
261
+ return `${year}-${month}-${day}_${hours}-${minutes}-${seconds}`;
262
+ }
263
+
264
+ /**
265
+ * Write test results to a JSON file
266
+ */
267
+ export function writeResultsToJson(
268
+ result: TestRunResult,
269
+ outputDir: string,
270
+ timestamp: string
271
+ ): string {
272
+ // Ensure output directory exists
273
+ if (!fs.existsSync(outputDir)) {
274
+ fs.mkdirSync(outputDir, { recursive: true });
275
+ }
276
+
277
+ const filename = `results_${timestamp}.json`;
278
+ const filepath = path.join(outputDir, filename);
279
+
280
+ // Write JSON with serializable data (strip non-serializable like RegExp)
281
+ const serializableResult = JSON.parse(JSON.stringify(result, (key, value) => {
282
+ if (value instanceof RegExp) {
283
+ return value.toString();
284
+ }
285
+ return value;
286
+ }));
287
+
288
+ fs.writeFileSync(filepath, JSON.stringify(serializableResult, null, 2));
289
+ return filepath;
290
+ }
291
+
292
+ /**
293
+ * Write a human-readable report to a text file
294
+ */
295
+ export function writeReportToFile(
296
+ result: TestRunResult,
297
+ outputDir: string,
298
+ timestamp: string
299
+ ): string {
300
+ // Ensure output directory exists
301
+ if (!fs.existsSync(outputDir)) {
302
+ fs.mkdirSync(outputDir, { recursive: true });
303
+ }
304
+
305
+ const filename = `report_${timestamp}.txt`;
306
+ const filepath = path.join(outputDir, filename);
307
+
308
+ const lines: string[] = [];
309
+
310
+ lines.push('=' .repeat(60));
311
+ lines.push('TRANSCRIPT TEST REPORT');
312
+ lines.push(`Generated: ${new Date().toISOString()}`);
313
+ lines.push('='.repeat(60));
314
+ lines.push('');
315
+
316
+ // Overall summary
317
+ const { totalPassed, totalFailed, totalExpectedFailures, totalSkipped, totalDuration } = result;
318
+ const total = totalPassed + totalFailed + totalExpectedFailures + totalSkipped;
319
+
320
+ lines.push(`Total: ${total} tests in ${result.transcripts.length} transcript(s)`);
321
+ lines.push(` Passed: ${totalPassed}`);
322
+ lines.push(` Failed: ${totalFailed}`);
323
+ lines.push(` Expected Failures: ${totalExpectedFailures}`);
324
+ lines.push(` Skipped: ${totalSkipped}`);
325
+ lines.push(` Duration: ${totalDuration}ms`);
326
+ lines.push('');
327
+
328
+ // Per-transcript details
329
+ for (const transcript of result.transcripts) {
330
+ lines.push('-'.repeat(60));
331
+ lines.push(`Transcript: ${transcript.transcript.filePath}`);
332
+ if (transcript.transcript.header.title) {
333
+ lines.push(` Title: ${transcript.transcript.header.title}`);
334
+ }
335
+ lines.push(` Results: ${transcript.passed} passed, ${transcript.failed} failed, ${transcript.expectedFailures} expected failures, ${transcript.skipped} skipped`);
336
+ lines.push('');
337
+
338
+ // Command details (only show failures in summary)
339
+ const failedCommands = transcript.commands.filter(c => !c.passed && !c.skipped && !c.expectedFailure);
340
+ if (failedCommands.length > 0) {
341
+ lines.push(' FAILURES:');
342
+ for (const cmd of failedCommands) {
343
+ lines.push(` Line ${cmd.command.lineNumber}: > ${cmd.command.input}`);
344
+ if (cmd.error) {
345
+ lines.push(` Error: ${cmd.error}`);
346
+ }
347
+ for (const ar of cmd.assertionResults) {
348
+ if (!ar.passed) {
349
+ lines.push(` - ${ar.message || formatAssertionPlain(ar.assertion)}`);
350
+ }
351
+ }
352
+ lines.push(' Actual output:');
353
+ for (const line of cmd.actualOutput.split('\n')) {
354
+ if (line.trim()) {
355
+ lines.push(` ${line}`);
356
+ }
357
+ }
358
+ lines.push('');
359
+ }
360
+ }
361
+ }
362
+
363
+ lines.push('='.repeat(60));
364
+ if (totalFailed === 0) {
365
+ lines.push('ALL TESTS PASSED');
366
+ } else {
367
+ lines.push(`${totalFailed} TEST(S) FAILED`);
368
+ }
369
+ lines.push('='.repeat(60));
370
+
371
+ fs.writeFileSync(filepath, lines.join('\n'));
372
+ return filepath;
373
+ }
374
+
375
+ /**
376
+ * Format an assertion without chalk colors (for file output)
377
+ */
378
+ function formatAssertionPlain(assertion: any): string {
379
+ switch (assertion.type) {
380
+ case 'ok':
381
+ return 'Exact match';
382
+ case 'ok-contains':
383
+ return `Contains "${assertion.value}"`;
384
+ case 'ok-not-contains':
385
+ return `Does not contain "${assertion.value}"`;
386
+ case 'ok-matches':
387
+ return `Matches ${assertion.pattern}`;
388
+ case 'fail':
389
+ return `Expected failure: ${assertion.reason}`;
390
+ case 'skip':
391
+ return `Skipped: ${assertion.reason || 'no reason given'}`;
392
+ case 'todo':
393
+ return `TODO: ${assertion.reason || 'not implemented'}`;
394
+ case 'event-count':
395
+ return `Event count: ${assertion.eventCount}`;
396
+ case 'event-assert': {
397
+ const prefix = assertion.assertTrue ? 'assertTrue' : 'assertFalse';
398
+ const posStr = assertion.eventPosition ? ` Event ${assertion.eventPosition}:` : '';
399
+ const dataStr = assertion.eventData ? ` ${JSON.stringify(assertion.eventData)}` : '';
400
+ return `${prefix}:${posStr} ${assertion.eventType}${dataStr}`;
401
+ }
402
+ case 'state-assert': {
403
+ const prefix = assertion.assertTrue ? 'assertTrue' : 'assertFalse';
404
+ return `${prefix}: ${assertion.stateExpression}`;
405
+ }
406
+ default:
407
+ return 'Unknown assertion';
408
+ }
409
+ }