feat(shorebird_cli): add shorebird preview command (#845)
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:scoped/scoped.dart';
|
||||
import 'package:shorebird_cli/src/adb.dart';
|
||||
import 'package:shorebird_cli/src/android_sdk.dart';
|
||||
import 'package:shorebird_cli/src/android_studio.dart';
|
||||
import 'package:shorebird_cli/src/auth/auth.dart';
|
||||
@@ -18,6 +19,7 @@ Future<void> main(List<String> args) async {
|
||||
await runScoped(
|
||||
() async => ShorebirdCliCommandRunner().run(args),
|
||||
values: {
|
||||
adbRef,
|
||||
androidSdkRef,
|
||||
androidStudioRef,
|
||||
authRef,
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:scoped/scoped.dart';
|
||||
import 'package:shorebird_cli/src/android_sdk.dart';
|
||||
import 'package:shorebird_cli/src/process.dart';
|
||||
|
||||
/// A reference to a [Adb] instance.
|
||||
final adbRef = create(Adb.new);
|
||||
|
||||
/// The [Adb] instance available in the current zone.
|
||||
Adb get adb => read(adbRef);
|
||||
|
||||
/// A wrapper around the `adb` command.
|
||||
class Adb {
|
||||
Future<ShorebirdProcessResult> _exec(String command) async {
|
||||
|
||||
@@ -65,6 +65,7 @@ class ShorebirdCliCommandRunner extends CompletionCommandRunner<int> {
|
||||
addCommand(LoginCiCommand());
|
||||
addCommand(LogoutCommand());
|
||||
addCommand(PatchCommand());
|
||||
addCommand(PreviewCommand());
|
||||
addCommand(ReleaseCommand());
|
||||
addCommand(ReleasesCommand());
|
||||
addCommand(RunCommand());
|
||||
|
||||
@@ -9,6 +9,7 @@ export 'login_ci_command.dart';
|
||||
export 'login_command.dart';
|
||||
export 'logout_command.dart';
|
||||
export 'patch/patch.dart';
|
||||
export 'preview_command.dart';
|
||||
export 'release/release.dart';
|
||||
export 'releases/releases.dart';
|
||||
export 'run_command.dart';
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:mason_logger/mason_logger.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:shorebird_cli/src/adb.dart';
|
||||
import 'package:shorebird_cli/src/bundletool.dart';
|
||||
import 'package:shorebird_cli/src/cache.dart';
|
||||
import 'package:shorebird_cli/src/code_push_client_wrapper.dart';
|
||||
import 'package:shorebird_cli/src/command.dart';
|
||||
import 'package:shorebird_cli/src/logger.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_config_mixin.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_validation_mixin.dart';
|
||||
import 'package:shorebird_cli/src/third_party/flutter_tools/lib/flutter_tools.dart';
|
||||
|
||||
/// {@template preview_command}
|
||||
/// `shorebird preview` command.
|
||||
/// {@endtemplate}
|
||||
class PreviewCommand extends ShorebirdCommand
|
||||
with ShorebirdConfigMixin, ShorebirdValidationMixin {
|
||||
/// {@macro preview_command}
|
||||
PreviewCommand({super.validators}) {
|
||||
argParser
|
||||
..addOption(
|
||||
'device-id',
|
||||
help: 'The ID of the device or simulator to preview the release on.',
|
||||
)
|
||||
..addOption(
|
||||
'app-id',
|
||||
help: 'The ID of the app to preview the release for.',
|
||||
)
|
||||
..addOption(
|
||||
'release-version',
|
||||
help: 'The version of the release (e.g. "1.0.0").',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String get name => 'preview';
|
||||
|
||||
@override
|
||||
String get description => 'Preview a specific release on a device.';
|
||||
|
||||
@override
|
||||
Future<int> run() async {
|
||||
try {
|
||||
await validatePreconditions(checkUserIsAuthenticated: true);
|
||||
} on PreconditionFailedException catch (error) {
|
||||
return error.exitCode.code;
|
||||
}
|
||||
|
||||
const platform = 'android';
|
||||
final appId = results['app-id'] as String? ?? await promptForApp();
|
||||
|
||||
if (appId == null) {
|
||||
logger.info('No apps found');
|
||||
return ExitCode.success.code;
|
||||
}
|
||||
|
||||
final releaseVersion = results['release-version'] as String? ??
|
||||
await promptForReleaseVersion(appId);
|
||||
|
||||
if (releaseVersion == null) {
|
||||
logger.info('No releases found');
|
||||
return ExitCode.success.code;
|
||||
}
|
||||
|
||||
final previewDirectory = cache.getPreviewDirectory(appId);
|
||||
final aabPath = p.join(
|
||||
previewDirectory.path,
|
||||
'${platform}_$releaseVersion.aab',
|
||||
);
|
||||
|
||||
if (!File(aabPath).existsSync()) {
|
||||
final downloadArtifactProgress = logger.progress('Downloading release');
|
||||
try {
|
||||
final release = await codePushClientWrapper.getRelease(
|
||||
appId: appId,
|
||||
releaseVersion: releaseVersion,
|
||||
);
|
||||
final releaseAabArtifact =
|
||||
await codePushClientWrapper.getReleaseArtifact(
|
||||
releaseId: release.id,
|
||||
// TODO(felangel): add iOS support
|
||||
arch: 'aab',
|
||||
platform: platform,
|
||||
);
|
||||
|
||||
await releaseAabArtifact.url.download(aabPath);
|
||||
downloadArtifactProgress.complete();
|
||||
} catch (error) {
|
||||
downloadArtifactProgress.fail('$error');
|
||||
return ExitCode.software.code;
|
||||
}
|
||||
}
|
||||
|
||||
final extractMetadataProgress = logger.progress('Extracting metadata');
|
||||
late String package;
|
||||
try {
|
||||
package = await bundletool.getPackageName(aabPath);
|
||||
extractMetadataProgress.complete();
|
||||
} catch (error) {
|
||||
extractMetadataProgress.fail('$error');
|
||||
return ExitCode.software.code;
|
||||
}
|
||||
|
||||
final apksPath = p.join(
|
||||
previewDirectory.path,
|
||||
'${platform}_$releaseVersion.apks',
|
||||
);
|
||||
|
||||
if (!File(apksPath).existsSync()) {
|
||||
final buildApksProgress = logger.progress('Building apks');
|
||||
try {
|
||||
await bundletool.buildApks(bundle: aabPath, output: apksPath);
|
||||
buildApksProgress.complete();
|
||||
} catch (error) {
|
||||
buildApksProgress.fail('$error');
|
||||
return ExitCode.software.code;
|
||||
}
|
||||
}
|
||||
|
||||
final installApksProgress = logger.progress('Installing apks');
|
||||
try {
|
||||
await bundletool.installApks(apks: apksPath);
|
||||
installApksProgress.complete();
|
||||
} catch (error) {
|
||||
installApksProgress.fail('$error');
|
||||
return ExitCode.software.code;
|
||||
}
|
||||
|
||||
final startAppProgress = logger.progress('Starting app');
|
||||
try {
|
||||
await adb.startApp(package);
|
||||
startAppProgress.complete();
|
||||
} catch (error) {
|
||||
startAppProgress.fail('$error');
|
||||
return ExitCode.software.code;
|
||||
}
|
||||
|
||||
final process = await adb.logcat(filter: 'flutter');
|
||||
process.stdout.listen((event) {
|
||||
logger.info(utf8.decode(event));
|
||||
});
|
||||
process.stderr.listen((event) {
|
||||
logger.err(utf8.decode(event));
|
||||
});
|
||||
|
||||
return process.exitCode;
|
||||
}
|
||||
|
||||
Future<String?> promptForApp() async {
|
||||
final apps = await codePushClientWrapper.getApps();
|
||||
if (apps.isEmpty) return null;
|
||||
final app = logger.chooseOne(
|
||||
'Which app would you like to preview?',
|
||||
choices: apps,
|
||||
display: (app) => app.displayName,
|
||||
);
|
||||
return app.appId;
|
||||
}
|
||||
|
||||
Future<String?> promptForReleaseVersion(String appId) async {
|
||||
final releases = await codePushClientWrapper.getReleases(appId: appId);
|
||||
if (releases.isEmpty) return null;
|
||||
final release = logger.chooseOne(
|
||||
'Which release would you like to preview?',
|
||||
choices: releases,
|
||||
display: (release) => release.version,
|
||||
);
|
||||
return release.version;
|
||||
}
|
||||
}
|
||||
|
||||
extension on String {
|
||||
Future<void> download(String path) async {
|
||||
final uri = Uri.parse(this);
|
||||
final client = HttpClient();
|
||||
final request = await client.getUrl(uri);
|
||||
final response = await request.close();
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('Failed to download artifact at $this');
|
||||
}
|
||||
final file = File(path)..createSync(recursive: true);
|
||||
await response.pipe(file.openWrite());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,410 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:args/args.dart';
|
||||
import 'package:mason_logger/mason_logger.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:scoped/scoped.dart';
|
||||
import 'package:shorebird_cli/src/adb.dart';
|
||||
import 'package:shorebird_cli/src/auth/auth.dart';
|
||||
import 'package:shorebird_cli/src/bundletool.dart';
|
||||
import 'package:shorebird_cli/src/cache.dart';
|
||||
import 'package:shorebird_cli/src/code_push_client_wrapper.dart';
|
||||
import 'package:shorebird_cli/src/commands/commands.dart';
|
||||
import 'package:shorebird_cli/src/logger.dart';
|
||||
import 'package:shorebird_code_push_client/shorebird_code_push_client.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
class _MockAdb extends Mock implements Adb {}
|
||||
|
||||
class _MockAppMetadata extends Mock implements AppMetadata {}
|
||||
|
||||
class _MockArgResults extends Mock implements ArgResults {}
|
||||
|
||||
class _MockAuth extends Mock implements Auth {}
|
||||
|
||||
class _MockBundletool extends Mock implements Bundletool {}
|
||||
|
||||
class _MockCache extends Mock implements Cache {}
|
||||
|
||||
class _MockCodePushClientWrapper extends Mock
|
||||
implements CodePushClientWrapper {}
|
||||
|
||||
class _MockHttpClient extends Mock implements HttpClient {}
|
||||
|
||||
class _MockHttpClientRequest extends Mock implements HttpClientRequest {}
|
||||
|
||||
class _MockHttpClientResponse extends Mock implements HttpClientResponse {}
|
||||
|
||||
class _MockLogger extends Mock implements Logger {}
|
||||
|
||||
class _MockProgress extends Mock implements Progress {}
|
||||
|
||||
class _MockProcess extends Mock implements Process {}
|
||||
|
||||
class _MockRelease extends Mock implements Release {}
|
||||
|
||||
class _MockReleaseArtifact extends Mock implements ReleaseArtifact {}
|
||||
|
||||
void main() {
|
||||
group(PreviewCommand, () {
|
||||
const appId = 'test-app-id';
|
||||
const appDisplayName = 'Test App';
|
||||
const platform = 'android';
|
||||
const releaseVersion = '1.2.3';
|
||||
const releaseId = 42;
|
||||
const releaseArtifactUrl = 'https://example.com/release.aab';
|
||||
const packageName = 'com.example.app';
|
||||
|
||||
late Adb adb;
|
||||
late AppMetadata app;
|
||||
late ArgResults argResults;
|
||||
late Auth auth;
|
||||
late Bundletool bundletool;
|
||||
late Cache cache;
|
||||
late CodePushClientWrapper codePushClientWrapper;
|
||||
late HttpClient httpClient;
|
||||
late HttpClientRequest httpClientRequest;
|
||||
late HttpClientResponse httpClientResponse;
|
||||
late Logger logger;
|
||||
late Directory previewDirectory;
|
||||
late Process process;
|
||||
late Progress progress;
|
||||
late Release release;
|
||||
late ReleaseArtifact releaseArtifact;
|
||||
late PreviewCommand command;
|
||||
|
||||
R runWithOverrides<R>(R Function() body) {
|
||||
return HttpOverrides.runZoned(
|
||||
() => runScoped(
|
||||
body,
|
||||
values: {
|
||||
adbRef.overrideWith(() => adb),
|
||||
authRef.overrideWith(() => auth),
|
||||
bundletoolRef.overrideWith(() => bundletool),
|
||||
cacheRef.overrideWith(() => cache),
|
||||
codePushClientWrapperRef.overrideWith(() => codePushClientWrapper),
|
||||
loggerRef.overrideWith(() => logger),
|
||||
},
|
||||
),
|
||||
createHttpClient: (_) => httpClient,
|
||||
);
|
||||
}
|
||||
|
||||
String aabPath() => p.join(
|
||||
previewDirectory.path,
|
||||
'${platform}_$releaseVersion.aab',
|
||||
);
|
||||
|
||||
String apksPath() => p.join(
|
||||
previewDirectory.path,
|
||||
'${platform}_$releaseVersion.apks',
|
||||
);
|
||||
|
||||
setUpAll(() {
|
||||
registerFallbackValue(Uri());
|
||||
registerFallbackValue(StreamController<List<int>>());
|
||||
});
|
||||
|
||||
setUp(() {
|
||||
adb = _MockAdb();
|
||||
app = _MockAppMetadata();
|
||||
argResults = _MockArgResults();
|
||||
auth = _MockAuth();
|
||||
bundletool = _MockBundletool();
|
||||
cache = _MockCache();
|
||||
codePushClientWrapper = _MockCodePushClientWrapper();
|
||||
httpClient = _MockHttpClient();
|
||||
httpClientRequest = _MockHttpClientRequest();
|
||||
httpClientResponse = _MockHttpClientResponse();
|
||||
logger = _MockLogger();
|
||||
previewDirectory = Directory.systemTemp.createTempSync();
|
||||
process = _MockProcess();
|
||||
progress = _MockProgress();
|
||||
release = _MockRelease();
|
||||
releaseArtifact = _MockReleaseArtifact();
|
||||
command = PreviewCommand()..testArgResults = argResults;
|
||||
|
||||
when(() => argResults['app-id']).thenReturn(appId);
|
||||
when(() => argResults['release-version']).thenReturn(releaseVersion);
|
||||
when(() => auth.isAuthenticated).thenReturn(true);
|
||||
when(() => cache.getPreviewDirectory(any())).thenReturn(previewDirectory);
|
||||
when(
|
||||
() => codePushClientWrapper.getApps(),
|
||||
).thenAnswer((_) async => [app]);
|
||||
when(
|
||||
() => codePushClientWrapper.getReleases(appId: appId),
|
||||
).thenAnswer((_) async => [release]);
|
||||
when(
|
||||
() => codePushClientWrapper.getRelease(
|
||||
appId: any(named: 'appId'),
|
||||
releaseVersion: any(named: 'releaseVersion'),
|
||||
),
|
||||
).thenAnswer((_) async => release);
|
||||
when(
|
||||
() => codePushClientWrapper.getReleaseArtifact(
|
||||
releaseId: any(named: 'releaseId'),
|
||||
arch: any(named: 'arch'),
|
||||
platform: any(named: 'platform'),
|
||||
),
|
||||
).thenAnswer((_) async => releaseArtifact);
|
||||
when(() => app.appId).thenReturn(appId);
|
||||
when(() => app.displayName).thenReturn(appDisplayName);
|
||||
when(() => release.id).thenReturn(releaseId);
|
||||
when(() => release.version).thenReturn(releaseVersion);
|
||||
when(() => releaseArtifact.url).thenReturn(releaseArtifactUrl);
|
||||
when(() => logger.progress(any())).thenReturn(progress);
|
||||
when(
|
||||
() => httpClient.getUrl(any()),
|
||||
).thenAnswer((_) async => httpClientRequest);
|
||||
when(
|
||||
() => httpClientRequest.close(),
|
||||
).thenAnswer((_) async => httpClientResponse);
|
||||
when(() => httpClientResponse.statusCode).thenReturn(HttpStatus.ok);
|
||||
when(() => httpClientResponse.pipe(any())).thenAnswer((_) async {});
|
||||
when(
|
||||
() => bundletool.getPackageName(any()),
|
||||
).thenAnswer((_) async => packageName);
|
||||
when(
|
||||
() => bundletool.buildApks(
|
||||
bundle: any(named: 'bundle'),
|
||||
output: any(named: 'output'),
|
||||
),
|
||||
).thenAnswer((_) async {});
|
||||
when(
|
||||
() => bundletool.installApks(apks: any(named: 'apks')),
|
||||
).thenAnswer((_) async {});
|
||||
when(() => adb.startApp(any())).thenAnswer((_) async {});
|
||||
when(
|
||||
() => adb.logcat(filter: any(named: 'filter')),
|
||||
).thenAnswer((_) async => process);
|
||||
when(
|
||||
() => process.exitCode,
|
||||
).thenAnswer((_) async => ExitCode.success.code);
|
||||
when(() => process.stdout).thenAnswer((_) => const Stream.empty());
|
||||
when(() => process.stderr).thenAnswer((_) => const Stream.empty());
|
||||
});
|
||||
|
||||
test('returns no user error when not logged in', () async {
|
||||
when(() => auth.isAuthenticated).thenReturn(false);
|
||||
final result = await runWithOverrides(command.run);
|
||||
expect(result, ExitCode.noUser.code);
|
||||
});
|
||||
|
||||
test('exits with code 70 when querying for release fails', () async {
|
||||
final exception = Exception('oops');
|
||||
when(
|
||||
() => codePushClientWrapper.getRelease(
|
||||
appId: any(named: 'appId'),
|
||||
releaseVersion: any(named: 'releaseVersion'),
|
||||
),
|
||||
).thenThrow(exception);
|
||||
final result = await runWithOverrides(command.run);
|
||||
expect(result, equals(ExitCode.software.code));
|
||||
verify(
|
||||
() => codePushClientWrapper.getRelease(
|
||||
appId: appId,
|
||||
releaseVersion: releaseVersion,
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('exits with code 70 when querying for release artifact fails',
|
||||
() async {
|
||||
final exception = Exception('oops');
|
||||
when(
|
||||
() => codePushClientWrapper.getReleaseArtifact(
|
||||
releaseId: any(named: 'releaseId'),
|
||||
arch: any(named: 'arch'),
|
||||
platform: any(named: 'platform'),
|
||||
),
|
||||
).thenThrow(exception);
|
||||
final result = await runWithOverrides(command.run);
|
||||
expect(result, equals(ExitCode.software.code));
|
||||
verify(
|
||||
() => codePushClientWrapper.getReleaseArtifact(
|
||||
releaseId: releaseId,
|
||||
arch: 'aab',
|
||||
platform: platform,
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('exits with code 70 when downloading release artifact fails',
|
||||
() async {
|
||||
final exception = Exception('oops');
|
||||
when(() => httpClient.getUrl(any())).thenThrow(exception);
|
||||
final result = await runWithOverrides(command.run);
|
||||
expect(result, equals(ExitCode.software.code));
|
||||
verify(() => httpClient.getUrl(Uri.parse(releaseArtifactUrl))).called(1);
|
||||
});
|
||||
|
||||
test(
|
||||
'exits with code 70 when downloading release artifact '
|
||||
'returns non-200 response', () async {
|
||||
when(
|
||||
() => httpClientResponse.statusCode,
|
||||
).thenReturn(HttpStatus.badRequest);
|
||||
final result = await runWithOverrides(command.run);
|
||||
expect(result, equals(ExitCode.software.code));
|
||||
verify(() => httpClientRequest.close()).called(1);
|
||||
verify(() => httpClientResponse.statusCode).called(1);
|
||||
});
|
||||
|
||||
test('exits with code 70 when extracting metadata fails', () async {
|
||||
final exception = Exception('oops');
|
||||
when(() => bundletool.getPackageName(any())).thenThrow(exception);
|
||||
final result = await runWithOverrides(command.run);
|
||||
expect(result, equals(ExitCode.software.code));
|
||||
verify(() => bundletool.getPackageName(aabPath())).called(1);
|
||||
});
|
||||
|
||||
test('exits with code 70 when building apks fails', () async {
|
||||
final exception = Exception('oops');
|
||||
when(
|
||||
() => bundletool.buildApks(
|
||||
bundle: any(named: 'bundle'),
|
||||
output: any(named: 'output'),
|
||||
),
|
||||
).thenThrow(exception);
|
||||
final result = await runWithOverrides(command.run);
|
||||
expect(result, equals(ExitCode.software.code));
|
||||
verify(
|
||||
() => bundletool.buildApks(bundle: aabPath(), output: apksPath()),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('exits with code 70 when installing apks fails', () async {
|
||||
final exception = Exception('oops');
|
||||
when(
|
||||
() => bundletool.installApks(apks: any(named: 'apks')),
|
||||
).thenThrow(exception);
|
||||
final result = await runWithOverrides(command.run);
|
||||
expect(result, equals(ExitCode.software.code));
|
||||
verify(() => bundletool.installApks(apks: apksPath())).called(1);
|
||||
});
|
||||
|
||||
test('exits with code 70 when starting app fails', () async {
|
||||
final exception = Exception('oops');
|
||||
when(() => adb.startApp(any())).thenThrow(exception);
|
||||
final result = await runWithOverrides(command.run);
|
||||
expect(result, equals(ExitCode.software.code));
|
||||
verify(() => adb.startApp(packageName)).called(1);
|
||||
});
|
||||
|
||||
test('exits with non-zero exit code when logcat process fails', () async {
|
||||
when(() => process.exitCode).thenAnswer((_) async => 1);
|
||||
final result = await runWithOverrides(command.run);
|
||||
expect(result, equals(1));
|
||||
verify(() => adb.logcat(filter: 'flutter')).called(1);
|
||||
});
|
||||
|
||||
test('pipes stdout output to logger', () async {
|
||||
final completer = Completer<int>();
|
||||
when(() => process.exitCode).thenAnswer((_) => completer.future);
|
||||
const output = 'hello world';
|
||||
when(
|
||||
() => process.stdout,
|
||||
).thenAnswer((_) => Stream.value(utf8.encode(output)));
|
||||
final result = runWithOverrides(command.run);
|
||||
completer.complete(0);
|
||||
await expectLater(await result, equals(ExitCode.success.code));
|
||||
verify(() => logger.info(output)).called(1);
|
||||
});
|
||||
|
||||
test('pipes stderr output to logger', () async {
|
||||
final completer = Completer<int>();
|
||||
when(() => process.exitCode).thenAnswer((_) => completer.future);
|
||||
const output = 'hello world';
|
||||
when(
|
||||
() => process.stderr,
|
||||
).thenAnswer((_) => Stream.value(utf8.encode(output)));
|
||||
final result = runWithOverrides(command.run);
|
||||
completer.complete(0);
|
||||
await expectLater(await result, equals(ExitCode.success.code));
|
||||
verify(() => logger.err(output)).called(1);
|
||||
});
|
||||
|
||||
test('queries for apps when app-id is not specified', () async {
|
||||
when(() => argResults['app-id']).thenReturn(null);
|
||||
when(
|
||||
() => logger.chooseOne<AppMetadata>(
|
||||
any(),
|
||||
choices: any(named: 'choices'),
|
||||
display: any(named: 'display'),
|
||||
),
|
||||
).thenReturn(app);
|
||||
final result = await runWithOverrides(command.run);
|
||||
expect(result, equals(ExitCode.success.code));
|
||||
final captured = verify(
|
||||
() => logger.chooseOne<AppMetadata>(
|
||||
any(),
|
||||
choices: any(named: 'choices'),
|
||||
display: captureAny(named: 'display'),
|
||||
),
|
||||
).captured.single as String Function(AppMetadata);
|
||||
expect(captured(app), equals(app.displayName));
|
||||
verify(() => codePushClientWrapper.getApps()).called(1);
|
||||
});
|
||||
|
||||
test('exits early when no apps are found', () async {
|
||||
when(() => argResults['app-id']).thenReturn(null);
|
||||
when(() => codePushClientWrapper.getApps()).thenAnswer((_) async => []);
|
||||
final result = await runWithOverrides(command.run);
|
||||
expect(result, equals(ExitCode.success.code));
|
||||
verifyNever(
|
||||
() => logger.chooseOne<AppMetadata>(
|
||||
any(),
|
||||
choices: any(named: 'choices'),
|
||||
display: captureAny(named: 'display'),
|
||||
),
|
||||
);
|
||||
verify(() => codePushClientWrapper.getApps()).called(1);
|
||||
verify(() => logger.info('No apps found')).called(1);
|
||||
});
|
||||
|
||||
test('exits early when no releases are found', () async {
|
||||
when(() => argResults['release-version']).thenReturn(null);
|
||||
when(
|
||||
() => codePushClientWrapper.getReleases(appId: any(named: 'appId')),
|
||||
).thenAnswer((_) async => []);
|
||||
final result = await runWithOverrides(command.run);
|
||||
expect(result, equals(ExitCode.success.code));
|
||||
verifyNever(
|
||||
() => logger.chooseOne<AppMetadata>(
|
||||
any(),
|
||||
choices: any(named: 'choices'),
|
||||
display: captureAny(named: 'display'),
|
||||
),
|
||||
);
|
||||
verify(() => codePushClientWrapper.getReleases(appId: appId)).called(1);
|
||||
verify(() => logger.info('No releases found')).called(1);
|
||||
});
|
||||
|
||||
test(
|
||||
'queries for releases when '
|
||||
'release-version is not specified', () async {
|
||||
when(() => argResults['release-version']).thenReturn(null);
|
||||
when(
|
||||
() => logger.chooseOne<Release>(
|
||||
any(),
|
||||
choices: any(named: 'choices'),
|
||||
display: any(named: 'display'),
|
||||
),
|
||||
).thenReturn(release);
|
||||
final result = await runWithOverrides(command.run);
|
||||
expect(result, equals(ExitCode.success.code));
|
||||
final captured = verify(
|
||||
() => logger.chooseOne<Release>(
|
||||
any(),
|
||||
choices: any(named: 'choices'),
|
||||
display: captureAny(named: 'display'),
|
||||
),
|
||||
).captured.single as String Function(Release);
|
||||
expect(captured(release), equals(releaseVersion));
|
||||
verify(() => codePushClientWrapper.getReleases(appId: appId)).called(1);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user