@@ -2,6 +2,7 @@ import 'dart:convert';
22import 'dart:io' ;
33import 'package:args/args.dart' ;
44import 'package:path/path.dart' as path;
5+ import 'package:recase/recase.dart' ;
56import '../console/console.dart' ;
67
78/// Tracks a test that has started but not yet finished
@@ -92,9 +93,6 @@ class TestCommand {
9293 if (coverage) args.add ('--coverage' );
9394 args.add (testPath);
9495
95- NyloConsole .writeStep ('Running tests...' );
96- NyloConsole .write ('' );
97-
9896 final process = await Process .start (
9997 'flutter' ,
10098 args,
@@ -105,6 +103,7 @@ class TestCommand {
105103 final tests = < int , _TestInfo > {};
106104 final testErrors = < int , String > {};
107105 final allResults = < _IndividualTestResult > [];
106+ final printedSuites = < String > {};
108107 final rawOutput = StringBuffer ();
109108 final stderrBuffer = StringBuffer ();
110109
@@ -117,7 +116,7 @@ class TestCommand {
117116 final result = _processJsonLine (line, suites, tests, testErrors);
118117 if (result != null ) {
119118 allResults.add (result);
120- _printTestResult (result);
119+ _printTestResult (result, printedSuites );
121120 }
122121 }),
123122 process.stderr.transform (utf8.decoder).forEach ((data) {
@@ -207,90 +206,84 @@ class TestCommand {
207206 return null ;
208207 }
209208
210- /// Print a formatted test result line
211- void _printTestResult (_IndividualTestResult result) {
209+ /// Convert a suite file path to a display name
210+ /// e.g. `test/auth_test.dart` → `Test\Auth`
211+ String _suiteDisplayName (String suitePath) {
212+ var relative = path.relative (suitePath);
213+ if (relative.endsWith ('.dart' )) {
214+ relative = relative.substring (0 , relative.length - 5 );
215+ }
216+ if (relative.endsWith ('_test' )) {
217+ relative = relative.substring (0 , relative.length - 5 );
218+ }
219+ final segments = path.split (relative);
220+ return segments.map ((s) => ReCase (s).pascalCase).join ('\\ ' );
221+ }
222+
223+ /// Print a single test result, printing suite header on first encounter
224+ void _printTestResult (
225+ _IndividualTestResult result, Set <String > printedSuites) {
226+ // Print suite header if this is the first test from this suite
227+ if (printedSuites.add (result.suitePath)) {
228+ final displayName = _suiteDisplayName (result.suitePath);
229+ stdout.writeln ('\n \x 1B[1m$displayName \x 1B[0m' );
230+ }
231+
212232 final termWidth = _getTerminalWidth ();
233+ final icon =
234+ result.passed ? '\x 1B[92m\u 2713\x 1B[0m' : '\x 1B[91m\u 2717\x 1B[0m' ;
213235 final durationStr = _formatTestDuration (result.duration);
214- final suitePath = path.relative (result.suitePath);
215236 final testName = result.testName;
216237
217- // Build visible label (no ANSI) for width calculations
218- final visibleLabel = '$suitePath > $testName ' ;
219238 final prefixLen = 4 ; // " ✓ " or " ✗ "
220- final availableWidth = termWidth - prefixLen - durationStr.length - 1 ;
239+ final availableWidth = termWidth - prefixLen - durationStr.length - 2 ;
221240
222- // Truncate if too long
223- String displayVisible;
224- if (availableWidth > 3 && visibleLabel.length > availableWidth) {
225- displayVisible = '${visibleLabel .substring (0 , availableWidth - 1 )}\u 2026' ;
241+ String displayTest;
242+ if (availableWidth > 3 && testName.length > availableWidth) {
243+ displayTest = '${testName .substring (0 , availableWidth - 1 )}\u 2026' ;
226244 } else {
227- displayVisible = visibleLabel ;
245+ displayTest = testName ;
228246 }
229247
230- // Pad for right-aligned duration
231248 final padLen =
232- termWidth - prefixLen - displayVisible .length - durationStr.length;
249+ termWidth - prefixLen - displayTest .length - durationStr.length;
233250 final pad = padLen > 0 ? ' ' * padLen : ' ' ;
234-
235- // Add ANSI color to the ">" separator
236- String ansiLabel;
237- if (displayVisible.length > suitePath.length + 3 ) {
238- final testPart = displayVisible.substring (suitePath.length + 3 );
239- ansiLabel = '$suitePath \x 1B[90m>\x 1B[0m $testPart ' ;
240- } else {
241- ansiLabel = displayVisible;
242- }
243-
244- final icon =
245- result.passed ? '\x 1B[92m\u 2713\x 1B[0m' : '\x 1B[91m\u 2717\x 1B[0m' ;
246251 final coloredDuration = _colorDuration (result.duration, durationStr);
247252
248- stdout.writeln (' $icon $ansiLabel $pad $coloredDuration ' );
253+ stdout.writeln (' $icon $displayTest $pad $coloredDuration ' );
254+
255+ // Print error details for failed tests
256+ if (! result.passed &&
257+ result.errorMessage != null &&
258+ result.errorMessage! .trim ().isNotEmpty) {
259+ final cleanError = _stripAnsiCodes (result.errorMessage! );
260+ for (final errorLine in cleanError.split ('\n ' )) {
261+ if (errorLine.trim ().isNotEmpty) {
262+ stdout.writeln ('\x 1B[91m $errorLine \x 1B[0m' );
263+ }
264+ }
265+ }
249266 }
250267
251268 /// Print aggregated test summary
252269 void _printSummary (List <_IndividualTestResult > results) {
253270 final passed = results.where ((r) => r.passed).length;
254271 final failed = results.where ((r) => ! r.passed).length;
255- final total = results.length;
256-
257- NyloConsole .write ('' );
258- NyloConsole .write (' ${'─' * 50 }' );
259- NyloConsole .write ('' );
260-
261- // Print failure details
262- if (failed > 0 ) {
263- NyloConsole .writeError ('Failed tests:' );
264- NyloConsole .write ('' );
265- for (final r in results.where ((r) => ! r.passed)) {
266- final suitePath = path.relative (r.suitePath);
267- NyloConsole .writeError (' $suitePath > ${r .testName }' );
268- if (r.errorMessage != null && r.errorMessage! .trim ().isNotEmpty) {
269- final cleanError = _stripAnsiCodes (r.errorMessage! );
270- for (final errorLine in cleanError.split ('\n ' )) {
271- if (errorLine.trim ().isNotEmpty) {
272- NyloConsole .write (' $errorLine ' );
273- }
274- }
275- }
276- NyloConsole .write ('' );
277- }
278- }
279272
280- final summary = 'Tests: $passed passed, $failed failed, $total total' ;
281273 final totalTime = results.fold <Duration >(
282274 Duration .zero,
283275 (sum, r) => sum + r.duration,
284276 );
285- final time = 'Time: ${ _formatDuration (totalTime )}' ;
277+ final durationStr = _formatTestDuration (totalTime);
286278
279+ NyloConsole .write ('' );
287280 if (failed > 0 ) {
288- stdout.writeln (' \x 1B[91m \u 2717 $ summary \x 1B[0m' );
289- stdout. writeln ( ' \x 1B[91m $ time \x 1B[0m' );
281+ stdout.writeln (
282+ ' \x 1B[1mTests: \x 1B[0m \x 1B[92m$ passed passed \x 1B[0m, \x 1B[91m$ failed failed \x 1B[0m' );
290283 } else {
291- NyloConsole .writeStepComplete (summary);
292- NyloConsole .write (' $time ' );
284+ stdout.writeln (' \x 1B[1mTests:\x 1B[0m \x 1B[92m$passed passed\x 1B[0m' );
293285 }
286+ stdout.writeln (' \x 1B[1mDuration:\x 1B[0m $durationStr ' );
294287 }
295288
296289 /// Color a duration string based on speed
@@ -307,18 +300,6 @@ class TestCommand {
307300 return '${seconds .toStringAsFixed (2 )}s' ;
308301 }
309302
310- /// Format duration for summary display
311- String _formatDuration (Duration duration) {
312- if (duration.inMinutes > 0 ) {
313- return '${duration .inMinutes }m ${duration .inSeconds .remainder (60 )}s' ;
314- }
315- if (duration.inSeconds > 0 ) {
316- final ms = duration.inMilliseconds.remainder (1000 );
317- return '${duration .inSeconds }.${(ms ~/ 100 )}s' ;
318- }
319- return '${duration .inMilliseconds }ms' ;
320- }
321-
322303 /// Strip ANSI escape codes and their literal representations from text
323304 String _stripAnsiCodes (String text) {
324305 // Strip actual ANSI escape sequences (byte 0x1B)
0 commit comments