feat: improve release selection UX with date display and truncation (#3493)
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:mason_logger/mason_logger.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:scoped_deps/scoped_deps.dart';
|
||||
@@ -20,6 +19,7 @@ import 'package:shorebird_cli/src/metadata/metadata.dart';
|
||||
import 'package:shorebird_cli/src/patch_diff_checker.dart';
|
||||
import 'package:shorebird_cli/src/platform.dart';
|
||||
import 'package:shorebird_cli/src/platform/platform.dart';
|
||||
import 'package:shorebird_cli/src/release_chooser.dart';
|
||||
import 'package:shorebird_cli/src/release_type.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_command.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_env.dart';
|
||||
@@ -486,10 +486,9 @@ Building patch with Flutter $flutterVersionString
|
||||
throw ProcessExit(ExitCode.usage.code);
|
||||
}
|
||||
|
||||
return logger.chooseOne<Release>(
|
||||
'Which release would you like to patch?',
|
||||
choices: [...releasesForPlatform.sortedBy((r) => r.createdAt).reversed],
|
||||
display: (r) => r.version,
|
||||
return chooseRelease(
|
||||
releases: releasesForPlatform,
|
||||
action: 'patch',
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import 'package:shorebird_cli/src/executables/executables.dart';
|
||||
import 'package:shorebird_cli/src/logging/logging.dart';
|
||||
import 'package:shorebird_cli/src/platform.dart';
|
||||
import 'package:shorebird_cli/src/platform/platform.dart';
|
||||
import 'package:shorebird_cli/src/release_chooser.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_command.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_env.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_process.dart';
|
||||
@@ -305,10 +306,9 @@ This is only applicable when previewing Android releases.''',
|
||||
|
||||
/// Prompts the user to choose a release version to preview.
|
||||
Future<String> promptForReleaseVersion(List<Release> releases) async {
|
||||
final release = logger.chooseOne(
|
||||
'Which release would you like to preview?',
|
||||
choices: releases,
|
||||
display: (release) => release.version,
|
||||
final release = chooseRelease(
|
||||
releases: releases,
|
||||
action: 'preview',
|
||||
);
|
||||
return release.version;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:archive/archive_io.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:mason_logger/mason_logger.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:shorebird_cli/src/artifact_manager.dart';
|
||||
@@ -11,6 +10,7 @@ import 'package:shorebird_cli/src/config/config.dart';
|
||||
import 'package:shorebird_cli/src/executables/bundletool.dart';
|
||||
import 'package:shorebird_cli/src/extensions/arg_results.dart';
|
||||
import 'package:shorebird_cli/src/logging/shorebird_logger.dart';
|
||||
import 'package:shorebird_cli/src/release_chooser.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_command.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_env.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_validator.dart';
|
||||
@@ -177,10 +177,9 @@ class GetApksCommand extends ShorebirdCommand {
|
||||
throw ProcessExit(ExitCode.usage.code);
|
||||
}
|
||||
|
||||
return logger.chooseOne<Release>(
|
||||
'Which release would you like to generate an apk for?',
|
||||
choices: releases.sortedBy((r) => r.createdAt).reversed.toList(),
|
||||
display: (r) => r.version,
|
||||
return chooseRelease(
|
||||
releases: releases,
|
||||
action: 'generate an apk for',
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:shorebird_cli/src/logging/logging.dart';
|
||||
import 'package:shorebird_code_push_client/shorebird_code_push_client.dart';
|
||||
|
||||
/// The maximum number of releases to display before offering to show all.
|
||||
const maxDisplayedReleases = 10;
|
||||
|
||||
const _months = [
|
||||
'Jan',
|
||||
'Feb',
|
||||
'Mar',
|
||||
'Apr',
|
||||
'May',
|
||||
'Jun',
|
||||
'Jul',
|
||||
'Aug',
|
||||
'Sep',
|
||||
'Oct',
|
||||
'Nov',
|
||||
'Dec',
|
||||
];
|
||||
|
||||
/// Sentinel release used to represent the "Show all releases" option.
|
||||
///
|
||||
/// We use a sentinel rather than `null` because `chooseOne` internally
|
||||
/// performs a null check and does not support nullable types.
|
||||
@visibleForTesting
|
||||
final showAllReleaseSentinel = Release(
|
||||
id: -1,
|
||||
appId: '',
|
||||
version: '',
|
||||
flutterRevision: '',
|
||||
flutterVersion: null,
|
||||
displayName: null,
|
||||
platformStatuses: const {},
|
||||
createdAt: DateTime(0),
|
||||
updatedAt: DateTime(0),
|
||||
);
|
||||
|
||||
/// Formats a [DateTime] as "Mon DD" (e.g. "Jan 28").
|
||||
///
|
||||
/// Could be replaced with `DateFormat('MMM d').format(date)` from
|
||||
/// `package:intl` if we ever add that dependency.
|
||||
String formatReleaseDate(DateTime date) {
|
||||
return '${_months[date.month - 1]} ${date.day}';
|
||||
}
|
||||
|
||||
/// Formats a release for display in the chooser.
|
||||
///
|
||||
/// The [showAllReleaseSentinel] is rendered as "Show all N releases...".
|
||||
String formatReleaseDisplay(Release r, {required int totalCount}) {
|
||||
if (identical(r, showAllReleaseSentinel)) {
|
||||
return '\u2193 Show all $totalCount releases...';
|
||||
}
|
||||
return '${r.version} (${formatReleaseDate(r.createdAt)})';
|
||||
}
|
||||
|
||||
/// Prompts the user to choose a release from [releases].
|
||||
///
|
||||
/// Releases are sorted by [Release.createdAt] descending (newest first).
|
||||
/// If there are more than [maxDisplayedReleases], only the most recent
|
||||
/// are shown initially with an option to show all.
|
||||
///
|
||||
/// The [action] parameter is used in the prompt message, e.g. "patch",
|
||||
/// "preview", or "generate an apk for".
|
||||
Release chooseRelease({
|
||||
required Iterable<Release> releases,
|
||||
required String action,
|
||||
}) {
|
||||
final sorted = releases.sortedBy((r) => r.createdAt).reversed.toList();
|
||||
final prompt = 'Which release would you like to $action?';
|
||||
String display(Release r) =>
|
||||
formatReleaseDisplay(r, totalCount: sorted.length);
|
||||
|
||||
if (sorted.length == 1) {
|
||||
logger.info('Using release ${display(sorted.single)}');
|
||||
return sorted.single;
|
||||
}
|
||||
|
||||
// Only truncate when it actually saves lines (sentinel takes a line too).
|
||||
if (sorted.length > maxDisplayedReleases + 1) {
|
||||
final truncated = [
|
||||
...sorted.take(maxDisplayedReleases),
|
||||
showAllReleaseSentinel, // "Show all releases..." option.
|
||||
];
|
||||
|
||||
final choice = logger.chooseOne<Release>(
|
||||
prompt,
|
||||
choices: truncated,
|
||||
display: display,
|
||||
);
|
||||
|
||||
if (!identical(choice, showAllReleaseSentinel)) return choice;
|
||||
}
|
||||
|
||||
return logger.chooseOne<Release>(
|
||||
prompt,
|
||||
choices: sorted,
|
||||
display: display,
|
||||
);
|
||||
}
|
||||
@@ -1028,6 +1028,23 @@ void main() {
|
||||
group('when release version is not specified', () {
|
||||
setUp(() {
|
||||
when(() => argResults.wasParsed('release-version')).thenReturn(false);
|
||||
// Need 2+ releases so chooseRelease prompts instead of auto-selecting.
|
||||
final olderRelease = Release(
|
||||
id: 99,
|
||||
appId: appId,
|
||||
version: '0.0.1',
|
||||
flutterRevision: flutterRevision,
|
||||
flutterVersion: flutterVersion,
|
||||
displayName: '0.0.1',
|
||||
platformStatuses: const {releasePlatform: ReleaseStatus.active},
|
||||
createdAt: DateTime(2022),
|
||||
updatedAt: DateTime(2022),
|
||||
);
|
||||
when(
|
||||
() => codePushClientWrapper.getReleases(
|
||||
appId: any(named: 'appId'),
|
||||
),
|
||||
).thenAnswer((_) async => [release, olderRelease]);
|
||||
});
|
||||
|
||||
test(
|
||||
@@ -1081,7 +1098,10 @@ void main() {
|
||||
final displayFunctionCapture = verificationResult.captured.flattened
|
||||
.whereType<String Function(Release)>()
|
||||
.first;
|
||||
expect(displayFunctionCapture(release), equals(release.version));
|
||||
expect(
|
||||
displayFunctionCapture(release),
|
||||
equals('${release.version} (Jan 1)'),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1144,18 +1164,15 @@ void main() {
|
||||
test('only lists and uses releases '
|
||||
'for the specified platform', () async {
|
||||
await expectLater(runWithOverrides(command.run), completes);
|
||||
final captured =
|
||||
verify(
|
||||
() => logger.chooseOne<Release>(
|
||||
any(),
|
||||
choices: captureAny(named: 'choices'),
|
||||
display: any(named: 'display'),
|
||||
),
|
||||
).captured.single
|
||||
as List<Release>;
|
||||
|
||||
expect(captured.length, equals(1));
|
||||
expect(captured.first.version, equals(releaseVersion));
|
||||
// Only one release matches the platform, so chooseRelease
|
||||
// auto-selects it without prompting.
|
||||
verifyNever(
|
||||
() => logger.chooseOne<Release>(
|
||||
any(),
|
||||
choices: any(named: 'choices'),
|
||||
display: any(named: 'display'),
|
||||
),
|
||||
);
|
||||
|
||||
verify(
|
||||
() => patcher.buildPatchArtifact(releaseVersion: releaseVersion),
|
||||
|
||||
@@ -255,6 +255,7 @@ void main() {
|
||||
when(() => app.displayName).thenReturn(appDisplayName);
|
||||
when(() => release.id).thenReturn(releaseId);
|
||||
when(() => release.version).thenReturn(releaseVersion);
|
||||
when(() => release.createdAt).thenReturn(DateTime(2023));
|
||||
when(() => release.platformStatuses).thenReturn({
|
||||
ReleasePlatform.android: ReleaseStatus.active,
|
||||
ReleasePlatform.ios: ReleaseStatus.active,
|
||||
@@ -1153,6 +1154,7 @@ channel: ${track.channel}
|
||||
otherRelease = MockRelease();
|
||||
when(() => otherRelease.appId).thenReturn(appId);
|
||||
when(() => otherRelease.version).thenReturn(releaseVersion);
|
||||
when(() => otherRelease.createdAt).thenReturn(DateTime(2023));
|
||||
when(() => otherRelease.displayName).thenReturn('2.0.0+1');
|
||||
when(() => otherRelease.platformStatuses).thenReturn({
|
||||
ReleasePlatform.macos: ReleaseStatus.active,
|
||||
@@ -1175,20 +1177,18 @@ channel: ${track.channel}
|
||||
).thenAnswer((_) async => [release, otherRelease]);
|
||||
});
|
||||
|
||||
test('only prompts for release versions that '
|
||||
test('only uses releases that '
|
||||
'support the current platform', () async {
|
||||
await runWithOverrides(command.run);
|
||||
final releases =
|
||||
verify(
|
||||
() => logger.chooseOne<Release>(
|
||||
any(),
|
||||
choices: captureAny(named: 'choices'),
|
||||
display: any(named: 'display'),
|
||||
),
|
||||
).captured.single
|
||||
as List<Release>;
|
||||
expect(releases.length, equals(1));
|
||||
expect(releases.first.version, equals(releaseVersion));
|
||||
// Only one release matches the platform, so chooseRelease
|
||||
// auto-selects it without prompting.
|
||||
verifyNever(
|
||||
() => logger.chooseOne<Release>(
|
||||
any(),
|
||||
choices: any(named: 'choices'),
|
||||
display: any(named: 'display'),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1309,6 +1309,22 @@ channel: ${track.channel}
|
||||
display: any(named: 'display'),
|
||||
),
|
||||
).thenReturn(release);
|
||||
|
||||
// Need 2+ releases so chooseRelease prompts instead of
|
||||
// auto-selecting.
|
||||
final otherRelease = MockRelease();
|
||||
when(() => otherRelease.id).thenReturn(999);
|
||||
when(() => otherRelease.version).thenReturn('0.0.1');
|
||||
when(() => otherRelease.createdAt).thenReturn(DateTime(2022));
|
||||
when(() => otherRelease.platformStatuses).thenReturn({
|
||||
ReleasePlatform.android: ReleaseStatus.active,
|
||||
});
|
||||
when(
|
||||
() => codePushClientWrapper.getReleases(
|
||||
appId: any(named: 'appId'),
|
||||
sideloadableOnly: any(named: 'sideloadableOnly'),
|
||||
),
|
||||
).thenAnswer((_) async => [release, otherRelease]);
|
||||
});
|
||||
|
||||
test('queries for releases', () async {
|
||||
@@ -1323,7 +1339,7 @@ channel: ${track.channel}
|
||||
),
|
||||
).captured.single
|
||||
as String Function(Release);
|
||||
expect(captured(release), equals(releaseVersion));
|
||||
expect(captured(release), equals('$releaseVersion (Jan 1)'));
|
||||
verify(
|
||||
() => codePushClientWrapper.getReleases(
|
||||
appId: appId,
|
||||
|
||||
@@ -145,6 +145,7 @@ void main() {
|
||||
|
||||
when(() => release.id).thenReturn(releaseId);
|
||||
when(() => release.version).thenReturn(releaseVersion);
|
||||
when(() => release.createdAt).thenReturn(DateTime(2023));
|
||||
|
||||
when(() => releaseArtifact.url).thenReturn(releaseArtifactUrl);
|
||||
|
||||
@@ -284,6 +285,17 @@ void main() {
|
||||
group('when release version is not specified', () {
|
||||
setUp(() {
|
||||
when(() => argResults.wasParsed('release-version')).thenReturn(false);
|
||||
// Need 2+ releases so chooseRelease prompts instead of auto-selecting.
|
||||
final otherRelease = MockRelease();
|
||||
when(() => otherRelease.id).thenReturn(999);
|
||||
when(() => otherRelease.version).thenReturn('0.0.1');
|
||||
when(() => otherRelease.createdAt).thenReturn(DateTime(2022));
|
||||
when(
|
||||
() => codePushClientWrapper.getReleases(
|
||||
appId: any(named: 'appId'),
|
||||
sideloadableOnly: any(named: 'sideloadableOnly'),
|
||||
),
|
||||
).thenAnswer((_) async => [release, otherRelease]);
|
||||
});
|
||||
|
||||
test('prompts for release', () async {
|
||||
@@ -299,7 +311,7 @@ void main() {
|
||||
).captured.single
|
||||
as String Function(Release);
|
||||
|
||||
expect(capturedDisplay(release), equals(releaseVersion));
|
||||
expect(capturedDisplay(release), equals('$releaseVersion (Jan 1)'));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,310 @@
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:scoped_deps/scoped_deps.dart';
|
||||
import 'package:shorebird_cli/src/logging/logging.dart';
|
||||
import 'package:shorebird_cli/src/release_chooser.dart';
|
||||
import 'package:shorebird_code_push_client/shorebird_code_push_client.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'mocks.dart';
|
||||
|
||||
Release _release(String version, DateTime createdAt) {
|
||||
return Release(
|
||||
id: version.hashCode,
|
||||
appId: 'app',
|
||||
version: version,
|
||||
flutterRevision: 'rev',
|
||||
flutterVersion: null,
|
||||
displayName: null,
|
||||
platformStatuses: const {},
|
||||
createdAt: createdAt,
|
||||
updatedAt: createdAt,
|
||||
);
|
||||
}
|
||||
|
||||
void main() {
|
||||
group('chooseRelease', () {
|
||||
late ShorebirdLogger logger;
|
||||
|
||||
R runWithOverrides<R>(R Function() body) {
|
||||
return runScoped(
|
||||
body,
|
||||
values: {loggerRef.overrideWith(() => logger)},
|
||||
);
|
||||
}
|
||||
|
||||
setUp(() {
|
||||
logger = MockShorebirdLogger();
|
||||
});
|
||||
|
||||
test('returns single release without prompting', () {
|
||||
final release = _release('1.0.0', DateTime(2025, 3, 15));
|
||||
|
||||
final result = runWithOverrides(
|
||||
() => chooseRelease(
|
||||
releases: [release],
|
||||
action: 'patch',
|
||||
),
|
||||
);
|
||||
|
||||
expect(result, equals(release));
|
||||
verify(
|
||||
() => logger.info('Using release 1.0.0 (Mar 15)'),
|
||||
).called(1);
|
||||
verifyNever(
|
||||
() => logger.chooseOne<Release>(
|
||||
any(),
|
||||
choices: any(named: 'choices'),
|
||||
display: any(named: 'display'),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('sorts releases newest first', () {
|
||||
final old = _release('1.0.0', DateTime(2025));
|
||||
final mid = _release('1.1.0', DateTime(2025, 6));
|
||||
final newest = _release('1.2.0', DateTime(2026));
|
||||
|
||||
when(
|
||||
() => logger.chooseOne<Release>(
|
||||
any(),
|
||||
choices: any(named: 'choices'),
|
||||
display: any(named: 'display'),
|
||||
),
|
||||
).thenReturn(newest);
|
||||
|
||||
runWithOverrides(
|
||||
() => chooseRelease(
|
||||
releases: [mid, old, newest],
|
||||
action: 'patch',
|
||||
),
|
||||
);
|
||||
|
||||
final captured =
|
||||
verify(
|
||||
() => logger.chooseOne<Release>(
|
||||
'Which release would you like to patch?',
|
||||
choices: captureAny(named: 'choices'),
|
||||
display: any(named: 'display'),
|
||||
),
|
||||
).captured.first
|
||||
as List<Release>;
|
||||
|
||||
expect(captured.first.version, equals('1.2.0'));
|
||||
expect(captured.last.version, equals('1.0.0'));
|
||||
});
|
||||
|
||||
test('displays version with date', () {
|
||||
final release = _release('1.2.3', DateTime(2025, 3, 15));
|
||||
final other = _release('1.2.2', DateTime(2025, 3));
|
||||
|
||||
when(
|
||||
() => logger.chooseOne<Release>(
|
||||
any(),
|
||||
choices: any(named: 'choices'),
|
||||
display: any(named: 'display'),
|
||||
),
|
||||
).thenReturn(release);
|
||||
|
||||
runWithOverrides(
|
||||
() => chooseRelease(
|
||||
releases: [release, other],
|
||||
action: 'preview',
|
||||
),
|
||||
);
|
||||
|
||||
final verification = verify(
|
||||
() => logger.chooseOne<Release>(
|
||||
'Which release would you like to preview?',
|
||||
choices: any(named: 'choices'),
|
||||
display: captureAny(named: 'display'),
|
||||
),
|
||||
);
|
||||
final displayFn = verification.captured.first as String Function(Release);
|
||||
expect(displayFn(release), equals('1.2.3 (Mar 15)'));
|
||||
});
|
||||
|
||||
test('shows truncated list when more than 10 releases', () {
|
||||
final releases = List.generate(
|
||||
15,
|
||||
(i) => _release('1.0.$i', DateTime(2025, 1, i + 1)),
|
||||
);
|
||||
|
||||
var callCount = 0;
|
||||
when(
|
||||
() => logger.chooseOne<Release>(
|
||||
any(),
|
||||
choices: any(named: 'choices'),
|
||||
display: any(named: 'display'),
|
||||
),
|
||||
).thenAnswer((invocation) {
|
||||
final choices = invocation.namedArguments[#choices] as List<Release>;
|
||||
callCount++;
|
||||
if (callCount == 1) {
|
||||
// 10 releases + 1 "show all" sentinel = 11.
|
||||
expect(choices, hasLength(11));
|
||||
expect(
|
||||
identical(choices.last, showAllReleaseSentinel),
|
||||
isTrue,
|
||||
);
|
||||
// Return the sentinel to trigger "show all".
|
||||
return choices.last;
|
||||
}
|
||||
// Second call: all 15 releases.
|
||||
expect(choices, hasLength(15));
|
||||
return choices.first;
|
||||
});
|
||||
|
||||
final result = runWithOverrides(
|
||||
() => chooseRelease(
|
||||
releases: releases,
|
||||
action: 'patch',
|
||||
),
|
||||
);
|
||||
|
||||
expect(result, isA<Release>());
|
||||
expect(identical(result, showAllReleaseSentinel), isFalse);
|
||||
expect(callCount, equals(2));
|
||||
});
|
||||
|
||||
test('returns directly when user picks from truncated list', () {
|
||||
final releases = List.generate(
|
||||
15,
|
||||
(i) => _release('1.0.$i', DateTime(2025, 1, i + 1)),
|
||||
);
|
||||
|
||||
when(
|
||||
() => logger.chooseOne<Release>(
|
||||
any(),
|
||||
choices: any(named: 'choices'),
|
||||
display: any(named: 'display'),
|
||||
),
|
||||
).thenAnswer((invocation) {
|
||||
final choices = invocation.namedArguments[#choices] as List<Release>;
|
||||
// Return a real release, not the sentinel.
|
||||
return choices.first;
|
||||
});
|
||||
|
||||
final result = runWithOverrides(
|
||||
() => chooseRelease(
|
||||
releases: releases,
|
||||
action: 'patch',
|
||||
),
|
||||
);
|
||||
|
||||
expect(result, isA<Release>());
|
||||
verify(
|
||||
() => logger.chooseOne<Release>(
|
||||
any(),
|
||||
choices: any(named: 'choices'),
|
||||
display: any(named: 'display'),
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('shows all 11 releases without truncation', () {
|
||||
// 11 releases = maxDisplayedReleases + 1, so truncation would not
|
||||
// save any lines (10 + sentinel = 11 lines vs 11 lines).
|
||||
final releases = List.generate(
|
||||
11,
|
||||
(i) => _release('1.0.$i', DateTime(2025, 1, i + 1)),
|
||||
);
|
||||
|
||||
when(
|
||||
() => logger.chooseOne<Release>(
|
||||
any(),
|
||||
choices: any(named: 'choices'),
|
||||
display: any(named: 'display'),
|
||||
),
|
||||
).thenAnswer((invocation) {
|
||||
final choices = invocation.namedArguments[#choices] as List<Release>;
|
||||
expect(choices, hasLength(11));
|
||||
expect(
|
||||
choices.every((r) => !identical(r, showAllReleaseSentinel)),
|
||||
isTrue,
|
||||
);
|
||||
return choices.first;
|
||||
});
|
||||
|
||||
runWithOverrides(
|
||||
() => chooseRelease(
|
||||
releases: releases,
|
||||
action: 'patch',
|
||||
),
|
||||
);
|
||||
|
||||
verify(
|
||||
() => logger.chooseOne<Release>(
|
||||
any(),
|
||||
choices: any(named: 'choices'),
|
||||
display: any(named: 'display'),
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('skips truncation when 10 or fewer releases', () {
|
||||
final releases = List.generate(
|
||||
5,
|
||||
(i) => _release('1.0.$i', DateTime(2025, 1, i + 1)),
|
||||
);
|
||||
|
||||
when(
|
||||
() => logger.chooseOne<Release>(
|
||||
any(),
|
||||
choices: any(named: 'choices'),
|
||||
display: any(named: 'display'),
|
||||
),
|
||||
).thenAnswer((invocation) {
|
||||
final choices = invocation.namedArguments[#choices] as List<Release>;
|
||||
expect(choices, hasLength(5));
|
||||
// No sentinel present.
|
||||
expect(
|
||||
choices.every((r) => !identical(r, showAllReleaseSentinel)),
|
||||
isTrue,
|
||||
);
|
||||
return choices.first;
|
||||
});
|
||||
|
||||
runWithOverrides(
|
||||
() => chooseRelease(
|
||||
releases: releases,
|
||||
action: 'patch',
|
||||
),
|
||||
);
|
||||
|
||||
verify(
|
||||
() => logger.chooseOne<Release>(
|
||||
any(),
|
||||
choices: any(named: 'choices'),
|
||||
display: any(named: 'display'),
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
group('formatReleaseDisplay', () {
|
||||
test('formats release with version and date', () {
|
||||
final release = _release('2.0.1', DateTime(2025, 12, 25));
|
||||
expect(
|
||||
formatReleaseDisplay(release, totalCount: 5),
|
||||
equals('2.0.1 (Dec 25)'),
|
||||
);
|
||||
});
|
||||
|
||||
test('formats show all sentinel', () {
|
||||
expect(
|
||||
formatReleaseDisplay(showAllReleaseSentinel, totalCount: 23),
|
||||
equals('\u2193 Show all 23 releases...'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('formatReleaseDate', () {
|
||||
test('formats date correctly', () {
|
||||
expect(formatReleaseDate(DateTime(2025, 1, 5)), equals('Jan 5'));
|
||||
expect(
|
||||
formatReleaseDate(DateTime(2025, 12, 31)),
|
||||
equals('Dec 31'),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user