Unverified Commit 79bc1bfa authored by Emmanuel Garcia's avatar Emmanuel Garcia Committed by GitHub

Add ability to wrap text messages in a box (#94391)

parent 139a4d39
...@@ -78,6 +78,8 @@ final List<GradleHandledError> gradleErrors = <GradleHandledError>[ ...@@ -78,6 +78,8 @@ final List<GradleHandledError> gradleErrors = <GradleHandledError>[
incompatibleKotlinVersionHandler, incompatibleKotlinVersionHandler,
]; ];
const String _boxTitle = 'Flutter Fix';
// Multidex error message. // Multidex error message.
@visibleForTesting @visibleForTesting
final GradleHandledError multidexErrorHandler = GradleHandledError( final GradleHandledError multidexErrorHandler = GradleHandledError(
...@@ -163,9 +165,9 @@ final GradleHandledError multidexErrorHandler = GradleHandledError( ...@@ -163,9 +165,9 @@ final GradleHandledError multidexErrorHandler = GradleHandledError(
} }
} }
} else { } else {
globals.printStatus( globals.printBox(
'Flutter multidex handling is disabled. If you wish to let the tool configure multidex, use the --mutidex flag.', 'Flutter multidex handling is disabled. If you wish to let the tool configure multidex, use the --mutidex flag.',
indent: 4, title: _boxTitle,
); );
} }
return GradleBuildStatus.exit; return GradleBuildStatus.exit;
...@@ -185,11 +187,11 @@ final GradleHandledError permissionDeniedErrorHandler = GradleHandledError( ...@@ -185,11 +187,11 @@ final GradleHandledError permissionDeniedErrorHandler = GradleHandledError(
required bool usesAndroidX, required bool usesAndroidX,
required bool multidexEnabled, required bool multidexEnabled,
}) async { }) async {
globals.printStatus('${globals.logger.terminal.warningMark} Gradle does not have execution permission.', emphasis: true); globals.printBox(
globals.printStatus( '${globals.logger.terminal.warningMark} Gradle does not have execution permission.\n'
'You should change the ownership of the project directory to your user, ' 'You should change the ownership of the project directory to your user, '
'or move the project to a directory with execute permissions.', 'or move the project to a directory with execute permissions.',
indent: 4, title: _boxTitle,
); );
return GradleBuildStatus.exit; return GradleBuildStatus.exit;
}, },
...@@ -252,9 +254,12 @@ final GradleHandledError r8FailureHandler = GradleHandledError( ...@@ -252,9 +254,12 @@ final GradleHandledError r8FailureHandler = GradleHandledError(
required bool usesAndroidX, required bool usesAndroidX,
required bool multidexEnabled, required bool multidexEnabled,
}) async { }) async {
globals.printStatus('${globals.logger.terminal.warningMark} The shrinker may have failed to optimize the Java bytecode.', emphasis: true); globals.printBox(
globals.printStatus('To disable the shrinker, pass the `--no-shrink` flag to this command.', indent: 4); '${globals.logger.terminal.warningMark} The shrinker may have failed to optimize the Java bytecode.\n'
globals.printStatus('To learn more, see: https://developer.android.com/studio/build/shrink-code', indent: 4); 'To disable the shrinker, pass the `--no-shrink` flag to this command.\n'
'To learn more, see: https://developer.android.com/studio/build/shrink-code',
title: _boxTitle,
);
return GradleBuildStatus.exit; return GradleBuildStatus.exit;
}, },
eventLabel: 'r8', eventLabel: 'r8',
...@@ -280,12 +285,13 @@ final GradleHandledError licenseNotAcceptedHandler = GradleHandledError( ...@@ -280,12 +285,13 @@ final GradleHandledError licenseNotAcceptedHandler = GradleHandledError(
final RegExp licenseFailure = RegExp(licenseNotAcceptedMatcher, multiLine: true); final RegExp licenseFailure = RegExp(licenseNotAcceptedMatcher, multiLine: true);
assert(licenseFailure != null); assert(licenseFailure != null);
final Match? licenseMatch = licenseFailure.firstMatch(line); final Match? licenseMatch = licenseFailure.firstMatch(line);
globals.printStatus( globals.printBox(
'${globals.logger.terminal.warningMark} Unable to download needed Android SDK components, as the ' '${globals.logger.terminal.warningMark} Unable to download needed Android SDK components, as the '
'following licenses have not been accepted:\n' 'following licenses have not been accepted: '
'${licenseMatch?.group(1)}\n\n' '${licenseMatch?.group(1)}\n\n'
'To resolve this, please run the following command in a Terminal:\n' 'To resolve this, please run the following command in a Terminal:\n'
'flutter doctor --android-licenses' 'flutter doctor --android-licenses',
title: _boxTitle,
); );
return GradleBuildStatus.exit; return GradleBuildStatus.exit;
}, },
...@@ -344,21 +350,23 @@ final GradleHandledError flavorUndefinedHandler = GradleHandledError( ...@@ -344,21 +350,23 @@ final GradleHandledError flavorUndefinedHandler = GradleHandledError(
} }
} }
} }
globals.printStatus( final String errorMessage = '${globals.logger.terminal.warningMark} Gradle project does not define a task suitable for the requested build.';
'\n${globals.logger.terminal.warningMark} Gradle project does not define a task suitable ' final File buildGradle = project.directory.childDirectory('android').childDirectory('app').childFile('build.gradle');
'for the requested build.'
);
if (productFlavors.isEmpty) { if (productFlavors.isEmpty) {
globals.printStatus( globals.printBox(
'The android/app/build.gradle file does not define ' '$errorMessage\n\n'
'The ${buildGradle.absolute.path} file does not define '
'any custom product flavors. ' 'any custom product flavors. '
'You cannot use the --flavor option.' 'You cannot use the --flavor option.',
title: _boxTitle,
); );
} else { } else {
globals.printStatus( globals.printBox(
'The android/app/build.gradle file defines product ' '$errorMessage\n\n'
'flavors: ${productFlavors.join(', ')} ' 'The ${buildGradle.absolute.path} file defines product '
'You must specify a --flavor option to select one of them.' 'flavors: ${productFlavors.join(', ')}. '
'You must specify a --flavor option to select one of them.',
title: _boxTitle,
); );
} }
return GradleBuildStatus.exit; return GradleBuildStatus.exit;
...@@ -389,7 +397,7 @@ final GradleHandledError minSdkVersion = GradleHandledError( ...@@ -389,7 +397,7 @@ final GradleHandledError minSdkVersion = GradleHandledError(
final Match? minSdkVersionMatch = _minSdkVersionPattern.firstMatch(line); final Match? minSdkVersionMatch = _minSdkVersionPattern.firstMatch(line);
assert(minSdkVersionMatch?.groupCount == 3); assert(minSdkVersionMatch?.groupCount == 3);
final String bold = globals.logger.terminal.bolden( final String textInBold = globals.logger.terminal.bolden(
'Fix this issue by adding the following to the file ${gradleFile.path}:\n' 'Fix this issue by adding the following to the file ${gradleFile.path}:\n'
'android {\n' 'android {\n'
' defaultConfig {\n' ' defaultConfig {\n'
...@@ -397,12 +405,12 @@ final GradleHandledError minSdkVersion = GradleHandledError( ...@@ -397,12 +405,12 @@ final GradleHandledError minSdkVersion = GradleHandledError(
' }\n' ' }\n'
'}\n' '}\n'
); );
globals.printStatus( globals.printBox(
'\n'
'The plugin ${minSdkVersionMatch?.group(3)} requires a higher Android SDK version.\n' 'The plugin ${minSdkVersionMatch?.group(3)} requires a higher Android SDK version.\n'
'$bold\n' '$textInBold\n'
"Note that your app won't be available to users running Android SDKs below ${minSdkVersionMatch?.group(2)}.\n" "Note that your app won't be available to users running Android SDKs below ${minSdkVersionMatch?.group(2)}.\n"
'Alternatively, try to find a version of this plugin that supports these lower versions of the Android SDK.' 'Alternatively, try to find a version of this plugin that supports these lower versions of the Android SDK.',
title: _boxTitle,
); );
return GradleBuildStatus.exit; return GradleBuildStatus.exit;
}, },
...@@ -426,7 +434,7 @@ final GradleHandledError transformInputIssue = GradleHandledError( ...@@ -426,7 +434,7 @@ final GradleHandledError transformInputIssue = GradleHandledError(
.childDirectory('android') .childDirectory('android')
.childDirectory('app') .childDirectory('app')
.childFile('build.gradle'); .childFile('build.gradle');
final String bold = globals.logger.terminal.bolden( final String textInBold = globals.logger.terminal.bolden(
'Fix this issue by adding the following to the file ${gradleFile.path}:\n' 'Fix this issue by adding the following to the file ${gradleFile.path}:\n'
'android {\n' 'android {\n'
' lintOptions {\n' ' lintOptions {\n'
...@@ -434,10 +442,10 @@ final GradleHandledError transformInputIssue = GradleHandledError( ...@@ -434,10 +442,10 @@ final GradleHandledError transformInputIssue = GradleHandledError(
' }\n' ' }\n'
'}' '}'
); );
globals.printStatus( globals.printBox(
'\n'
'This issue appears to be https://github.com/flutter/flutter/issues/58247.\n' 'This issue appears to be https://github.com/flutter/flutter/issues/58247.\n'
'$bold' '$textInBold',
title: _boxTitle,
); );
return GradleBuildStatus.exit; return GradleBuildStatus.exit;
}, },
...@@ -459,14 +467,14 @@ final GradleHandledError lockFileDepMissing = GradleHandledError( ...@@ -459,14 +467,14 @@ final GradleHandledError lockFileDepMissing = GradleHandledError(
final File gradleFile = project.directory final File gradleFile = project.directory
.childDirectory('android') .childDirectory('android')
.childFile('build.gradle'); .childFile('build.gradle');
final String bold = globals.logger.terminal.bolden( final String textInBold = globals.logger.terminal.bolden(
'To regenerate the lockfiles run: `./gradlew :generateLockfiles` in ${gradleFile.path}\n' 'To regenerate the lockfiles run: `./gradlew :generateLockfiles` in ${gradleFile.path}\n'
'To remove dependency locking, remove the `dependencyLocking` from ${gradleFile.path}\n' 'To remove dependency locking, remove the `dependencyLocking` from ${gradleFile.path}'
); );
globals.printStatus( globals.printBox(
'\n'
'You need to update the lockfile, or disable Gradle dependency locking.\n' 'You need to update the lockfile, or disable Gradle dependency locking.\n'
'$bold' '$textInBold',
title: _boxTitle,
); );
return GradleBuildStatus.exit; return GradleBuildStatus.exit;
}, },
...@@ -487,15 +495,11 @@ final GradleHandledError incompatibleKotlinVersionHandler = GradleHandledError( ...@@ -487,15 +495,11 @@ final GradleHandledError incompatibleKotlinVersionHandler = GradleHandledError(
final File gradleFile = project.directory final File gradleFile = project.directory
.childDirectory('android') .childDirectory('android')
.childFile('build.gradle'); .childFile('build.gradle');
globals.printStatus( globals.printBox(
'${globals.logger.terminal.warningMark} Your project requires a newer version of the Kotlin Gradle plugin.', '${globals.logger.terminal.warningMark} Your project requires a newer version of the Kotlin Gradle plugin.\n'
emphasis: true, 'Find the latest version on https://kotlinlang.org/docs/gradle.html#plugin-and-versions, then update ${gradleFile.path}:\n'
);
globals.printStatus(
'Find the latest version on https://kotlinlang.org/docs/gradle.html#plugin-and-versions, '
'then update ${gradleFile.path}:\n'
"ext.kotlin_version = '<latest-version>'", "ext.kotlin_version = '<latest-version>'",
indent: 4, title: _boxTitle,
); );
return GradleBuildStatus.exit; return GradleBuildStatus.exit;
}, },
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async'; import 'dart:async';
import 'dart:math';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
...@@ -163,6 +164,32 @@ abstract class Logger { ...@@ -163,6 +164,32 @@ abstract class Logger {
bool? wrap, bool? wrap,
}); });
/// Display the [message] inside a box.
///
/// For example, this is the generated output:
///
/// ┌─ [title] ─┐
/// │ [message] │
/// └───────────┘
///
/// If a terminal is attached, the lines in [message] are automatically wrapped based on
/// the available columns.
///
/// Use this utility only to highlight a message in the logs.
///
/// This is particularly useful when the message can be easily missed because of clutter
/// generated by other commands invoked by the tool.
///
/// One common use case is to provide actionable steps in a Flutter app when a Gradle
/// error is printed.
///
/// In the future, this output can be integrated with an IDE like VS Code to display a
/// notification, and allow the user to trigger an action. e.g. run a migration.
void printBox(
String message, {
String? title,
});
/// Use this for verbose tracing output. Users can turn this output on in order /// Use this for verbose tracing output. Users can turn this output on in order
/// to help diagnose issues with the toolchain or with their setup. /// to help diagnose issues with the toolchain or with their setup.
void printTrace(String message); void printTrace(String message);
...@@ -312,6 +339,13 @@ class DelegatingLogger implements Logger { ...@@ -312,6 +339,13 @@ class DelegatingLogger implements Logger {
); );
} }
@override
void printBox(String message, {
String? title,
}) {
_delegate.printBox(message, title: title);
}
@override @override
void printTrace(String message) { void printTrace(String message) {
_delegate.printTrace(message); _delegate.printTrace(message);
...@@ -479,6 +513,21 @@ class StdoutLogger extends Logger { ...@@ -479,6 +513,21 @@ class StdoutLogger extends Logger {
_status?.resume(); _status?.resume();
} }
@override
void printBox(String message, {
String? title,
}) {
_status?.pause();
_generateBox(
title: title,
message: message,
wrapColumn: _outputPreferences.wrapColumn,
terminal: terminal,
write: writeToStdOut,
);
_status?.resume();
}
@protected @protected
void writeToStdOut(String message) => _stdio.stdoutWrite(message); void writeToStdOut(String message) => _stdio.stdoutWrite(message);
...@@ -558,6 +607,77 @@ class StdoutLogger extends Logger { ...@@ -558,6 +607,77 @@ class StdoutLogger extends Logger {
} }
} }
typedef _Writter = void Function(String message);
/// Wraps the message in a box, and writes the bytes by calling [write].
///
/// Example output:
///
/// ┌─ [title] ─┐
/// │ [message] │
/// └───────────┘
///
/// When [title] is provided, the box will have a title above it.
///
/// The box width never exceeds [wrapColumn].
///
/// If [wrapColumn] is not provided, the default value is 100.
void _generateBox({
required String message,
required int wrapColumn,
required _Writter write,
required Terminal terminal,
String? title,
}) {
const int kPaddingLeftRight = 1;
const int kEdges = 2;
final int maxTextWidthPerLine = wrapColumn - kEdges - kPaddingLeftRight * 2;
final List<String> lines = wrapText(message, shouldWrap: true, columnWidth: maxTextWidthPerLine).split('\n');
final List<int> lineWidth = lines.map((String line) => _getColumnSize(line)).toList();
final int maxColumnSize = lineWidth.reduce((int currLen, int maxLen) => max(currLen, maxLen));
final int textWidth = min(maxColumnSize, maxTextWidthPerLine);
final int textWithPaddingWidth = textWidth + kPaddingLeftRight * 2;
write('\n');
// Write `┌─ [title] ─┐`.
write('┌');
write('─');
if (title == null) {
write('─' * (textWithPaddingWidth - 1));
} else {
write(' ${terminal.bolden(title)} ');
write('─' * (textWithPaddingWidth - title.length - 3));
}
write('┐');
write('\n');
// Write `│ [message] │`.
for (int lineIdx = 0; lineIdx < lines.length; lineIdx++) {
write('│');
write(' ' * kPaddingLeftRight);
write(lines[lineIdx]);
final int remainingSpacesToEnd = textWidth - lineWidth[lineIdx];
write(' ' * (remainingSpacesToEnd + kPaddingLeftRight));
write('│');
write('\n');
}
// Write `└───────────┘`.
write('└');
write('─' * textWithPaddingWidth);
write('┘');
write('\n');
}
final RegExp _ansiEscapePattern = RegExp('\x1B\\[[\x30-\x3F]*[\x20-\x2F]*[\x40-\x7E]');
int _getColumnSize(String line) {
// Remove ANSI escape characters from the string.
return line.replaceAll(_ansiEscapePattern, '').length;
}
/// A [StdoutLogger] which replaces Unicode characters that cannot be printed to /// A [StdoutLogger] which replaces Unicode characters that cannot be printed to
/// the Windows console with alternative symbols. /// the Windows console with alternative symbols.
/// ///
...@@ -709,6 +829,19 @@ class BufferLogger extends Logger { ...@@ -709,6 +829,19 @@ class BufferLogger extends Logger {
} }
} }
@override
void printBox(String message, {
String? title,
}) {
_generateBox(
title: title,
message: message,
wrapColumn: _outputPreferences.wrapColumn,
terminal: terminal,
write: _status.write,
);
}
@override @override
void printTrace(String message) => _trace.writeln(message); void printTrace(String message) => _trace.writeln(message);
...@@ -830,6 +963,23 @@ class VerboseLogger extends DelegatingLogger { ...@@ -830,6 +963,23 @@ class VerboseLogger extends DelegatingLogger {
)); ));
} }
@override
void printBox(String message, {
String? title,
}) {
String composedMessage = '';
_generateBox(
title: title,
message: message,
wrapColumn: _outputPreferences.wrapColumn,
terminal: terminal,
write: (String line) {
composedMessage += line;
},
);
_emit(_LogType.status, composedMessage);
}
@override @override
void printTrace(String message) { void printTrace(String message) {
_emit(_LogType.trace, message); _emit(_LogType.trace, message);
......
...@@ -1036,6 +1036,13 @@ class NotifyingLogger extends DelegatingLogger { ...@@ -1036,6 +1036,13 @@ class NotifyingLogger extends DelegatingLogger {
_sendMessage(LogMessage('status', message)); _sendMessage(LogMessage('status', message));
} }
@override
void printBox(String message, {
String title,
}) {
_sendMessage(LogMessage('status', title == null ? message : '$title: $message'));
}
@override @override
void printTrace(String message) { void printTrace(String message) {
if (!verbose) { if (!verbose) {
......
...@@ -204,6 +204,23 @@ void printStatus( ...@@ -204,6 +204,23 @@ void printStatus(
); );
} }
/// Display the [message] inside a box.
///
/// For example, this is the generated output:
///
/// ┌─ [title] ─┐
/// │ [message] │
/// └───────────┘
///
/// If a terminal is attached, the lines in [message] are automatically wrapped based on
/// the available columns.
void printBox(String message, {
String? title,
}) {
logger.printBox(message, title: title);
}
/// Use this for verbose tracing output. Users can turn this output on in order /// Use this for verbose tracing output. Users can turn this output on in order
/// to help diagnose issues with the toolchain or with their setup. /// to help diagnose issues with the toolchain or with their setup.
void printTrace(String message) => logger.printTrace(message); void printTrace(String message) => logger.printTrace(message);
......
...@@ -598,6 +598,18 @@ class StreamLogger extends Logger { ...@@ -598,6 +598,18 @@ class StreamLogger extends Logger {
_log('[stdout] $message'); _log('[stdout] $message');
} }
@override
void printBox(
String message, {
String title,
}) {
if (title == null) {
_log('[stdout] $message');
} else {
_log('[stdout] $title: $message');
}
}
@override @override
void printTrace(String message) { void printTrace(String message) {
_log('[verbose] $message'); _log('[verbose] $message');
......
...@@ -172,6 +172,25 @@ void main() { ...@@ -172,6 +172,25 @@ void main() {
Logger: () => notifyingLogger, Logger: () => notifyingLogger,
}); });
testUsingContext('printBox should log to stdout when logToStdout is enabled', () async {
final StringBuffer buffer = await capturedConsolePrint(() {
final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>();
final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>();
daemon = Daemon(
commands.stream,
responses.add,
notifyingLogger: notifyingLogger,
logToStdout: true,
);
globals.printBox('This is the box message', title: 'Sample title');
return Future<void>.value();
});
expect(buffer.toString().trim(), contains('Sample title: This is the box message'));
}, overrides: <Type, Generator>{
Logger: () => notifyingLogger,
});
testUsingContext('daemon.shutdown command should stop daemon', () async { testUsingContext('daemon.shutdown command should stop daemon', () async {
final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>(); final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>();
final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>(); final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>();
......
...@@ -282,6 +282,31 @@ void main() { ...@@ -282,6 +282,31 @@ void main() {
mockLogger.errorText, mockLogger.errorText,
matches('^$red' r'\[ (?: {0,2}\+[0-9]{1,4} ms| )\] ' '${bold}Helpless!$resetBold$resetColor' r'\n$')); matches('^$red' r'\[ (?: {0,2}\+[0-9]{1,4} ms| )\] ' '${bold}Helpless!$resetBold$resetColor' r'\n$'));
}); });
testWithoutContext('printBox', () {
final BufferLogger mockLogger = BufferLogger(
terminal: AnsiTerminal(
stdio: FakeStdio(),
platform: FakePlatform(stdoutSupportsAnsi: true),
),
outputPreferences: OutputPreferences.test(showColor: true),
);
final VerboseLogger verboseLogger = VerboseLogger(
mockLogger, stopwatchFactory: FakeStopwatchFactory(stopwatch: fakeStopWatch),
);
verboseLogger.printBox('This is the box message', title: 'Sample title');
expect(
mockLogger.statusText,
contains('[ ] \x1B[1m\x1B[22m\n'
'\x1B[1m ┌─ Sample title ──────────┐\x1B[22m\n'
'\x1B[1m │ This is the box message │\x1B[22m\n'
'\x1B[1m └─────────────────────────┘\x1B[22m\n'
'\x1B[1m \x1B[22m\n'
),
);
});
}); });
testWithoutContext('Logger does not throw when stdio write throws synchronously', () async { testWithoutContext('Logger does not throw when stdio write throws synchronously', () async {
...@@ -928,6 +953,184 @@ void main() { ...@@ -928,6 +953,184 @@ void main() {
expect(lines[0], equals('')); expect(lines[0], equals(''));
}); });
testWithoutContext('Stdout printBox puts content inside a box', () {
final Logger logger = StdoutLogger(
terminal: AnsiTerminal(
stdio: fakeStdio,
platform: FakePlatform(),
),
stdio: fakeStdio,
outputPreferences: OutputPreferences.test(showColor: true),
);
logger.printBox('Hello world', title: 'Test title');
final String stdout = fakeStdio.writtenToStdout.join('');
expect(stdout,
contains(
'\n'
'┌─ Test title ┐\n'
'│ Hello world │\n'
'└─────────────┘\n'
),
);
});
testWithoutContext('Stdout printBox does not require title', () {
final Logger logger = StdoutLogger(
terminal: AnsiTerminal(
stdio: fakeStdio,
platform: FakePlatform(),
),
stdio: fakeStdio,
outputPreferences: OutputPreferences.test(showColor: true),
);
logger.printBox('Hello world');
final String stdout = fakeStdio.writtenToStdout.join('');
expect(stdout,
contains(
'\n'
'┌─────────────┐\n'
'│ Hello world │\n'
'└─────────────┘\n'
),
);
});
testWithoutContext('Stdout printBox handles new lines', () {
final Logger logger = StdoutLogger(
terminal: AnsiTerminal(
stdio: fakeStdio,
platform: FakePlatform(),
),
stdio: fakeStdio,
outputPreferences: OutputPreferences.test(showColor: true),
);
logger.printBox('Hello world\nThis is a new line', title: 'Test title');
final String stdout = fakeStdio.writtenToStdout.join('');
expect(stdout,
contains(
'\n'
'┌─ Test title ───────┐\n'
'│ Hello world │\n'
'│ This is a new line │\n'
'└────────────────────┘\n'
),
);
});
testWithoutContext('Stdout printBox handles content with ANSI escape characters', () {
final Logger logger = StdoutLogger(
terminal: AnsiTerminal(
stdio: fakeStdio,
platform: FakePlatform(),
),
stdio: fakeStdio,
outputPreferences: OutputPreferences.test(showColor: true),
);
const String bold = '\u001B[1m';
const String clear = '\u001B[2J\u001B[H';
logger.printBox('${bold}Hello world$clear', title: 'Test title');
final String stdout = fakeStdio.writtenToStdout.join('');
expect(stdout,
contains(
'\n'
'┌─ Test title ┐\n'
'│ ${bold}Hello world$clear\n'
'└─────────────┘\n'
),
);
});
testWithoutContext('Stdout printBox handles column limit', () {
const int columnLimit = 14;
final Logger logger = StdoutLogger(
terminal: AnsiTerminal(
stdio: fakeStdio,
platform: FakePlatform(),
),
stdio: fakeStdio,
outputPreferences: OutputPreferences.test(showColor: true, wrapColumn: columnLimit),
);
logger.printBox('This line is longer than $columnLimit characters', title: 'Test');
final String stdout = fakeStdio.writtenToStdout.join('');
final List<String> stdoutLines = stdout.split('\n');
expect(stdoutLines.length, greaterThan(1));
expect(stdoutLines[1].length, equals(columnLimit));
expect(stdout,
contains(
'\n'
'┌─ Test ─────┐\n'
'│ This line │\n'
'│ is longer │\n'
'│ than 14 │\n'
'│ characters │\n'
'└────────────┘\n'
),
);
});
testWithoutContext('Stdout printBox handles column limit and respects new lines', () {
const int columnLimit = 14;
final Logger logger = StdoutLogger(
terminal: AnsiTerminal(
stdio: fakeStdio,
platform: FakePlatform(),
),
stdio: fakeStdio,
outputPreferences: OutputPreferences.test(showColor: true, wrapColumn: columnLimit),
);
logger.printBox('This\nline is longer than\n\n$columnLimit characters', title: 'Test');
final String stdout = fakeStdio.writtenToStdout.join('');
final List<String> stdoutLines = stdout.split('\n');
expect(stdoutLines.length, greaterThan(1));
expect(stdoutLines[1].length, equals(columnLimit));
expect(stdout,
contains(
'\n'
'┌─ Test ─────┐\n'
'│ This │\n'
'│ line is │\n'
'│ longer │\n'
'│ than │\n'
'│ │\n'
'│ 14 │\n'
'│ characters │\n'
'└────────────┘\n'
),
);
});
testWithoutContext('Stdout printBox breaks long words that exceed the column limit', () {
const int columnLimit = 14;
final Logger logger = StdoutLogger(
terminal: AnsiTerminal(
stdio: fakeStdio,
platform: FakePlatform(),
),
stdio: fakeStdio,
outputPreferences: OutputPreferences.test(showColor: true, wrapColumn: columnLimit),
);
logger.printBox('Thiswordislongerthan${columnLimit}characters', title: 'Test');
final String stdout = fakeStdio.writtenToStdout.join('');
final List<String> stdoutLines = stdout.split('\n');
expect(stdoutLines.length, greaterThan(1));
expect(stdoutLines[1].length, equals(columnLimit));
expect(stdout,
contains(
'\n'
'┌─ Test ─────┐\n'
'│ Thiswordis │\n'
'│ longerthan │\n'
'│ 14characte │\n'
'│ rs │\n'
'└────────────┘\n'
),
);
});
testWithoutContext('Stdout startProgress on non-color terminal', () async { testWithoutContext('Stdout startProgress on non-color terminal', () async {
final FakeStopwatch fakeStopwatch = FakeStopwatch(); final FakeStopwatch fakeStopwatch = FakeStopwatch();
final Logger logger = StdoutLogger( final Logger logger = StdoutLogger(
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment