diff --git a/packages/shorebird_cli/lib/src/commands/preview_command.dart b/packages/shorebird_cli/lib/src/commands/preview_command.dart index bffa1e25..48cc399c 100644 --- a/packages/shorebird_cli/lib/src/commands/preview_command.dart +++ b/packages/shorebird_cli/lib/src/commands/preview_command.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:convert'; +import 'package:collection/collection.dart'; import 'package:mason_logger/mason_logger.dart'; import 'package:path/path.dart' as p; import 'package:shorebird_cli/src/adb.dart'; @@ -33,6 +34,15 @@ class PreviewCommand extends ShorebirdCommand ..addOption( 'release-version', help: 'The version of the release (e.g. "1.0.0").', + ) + ..addOption( + 'platform', + allowed: [ReleasePlatform.android.name, ReleasePlatform.ios.name], + allowedHelp: { + ReleasePlatform.android.name: 'Android', + ReleasePlatform.ios.name: 'iOS', + }, + help: 'The platform of the release.', ); } @@ -52,7 +62,6 @@ class PreviewCommand extends ShorebirdCommand return error.exitCode.code; } - const platform = ReleasePlatform.android; final appId = results['app-id'] as String? ?? await promptForApp(); if (appId == null) { @@ -60,32 +69,75 @@ class PreviewCommand extends ShorebirdCommand return ExitCode.success.code; } - final releaseVersion = results['release-version'] as String? ?? - await promptForReleaseVersion(appId); + final releases = await codePushClientWrapper.getReleases(appId: appId); - if (releaseVersion == null) { + final releaseVersion = results['release-version'] as String? ?? + await promptForReleaseVersion(releases); + + final release = releases.firstWhereOrNull( + (r) => r.version == releaseVersion, + ); + + if (releaseVersion == null || release == null) { logger.info('No releases found'); return ExitCode.success.code; } + final platform = ReleasePlatform.values.byName( + results['platform'] as String? ?? await promptForPlatform(release), + ); + + return switch (platform) { + ReleasePlatform.android => installAndLaunchAndroid(appId, release), + ReleasePlatform.ios => installAndLaunchIos(appId, release), + }; + } + + Future 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 promptForReleaseVersion(List releases) async { + 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; + } + + Future promptForPlatform(Release release) async { + final platforms = release.platformStatuses.keys.map((p) => p.name).toList(); + final platform = logger.chooseOne( + 'Which platform would you like to preview?', + choices: platforms, + ); + return platform; + } + + Future installAndLaunchAndroid(String appId, Release release) async { + const platform = ReleasePlatform.android; final previewDirectory = cache.getPreviewDirectory(appId); final aabPath = p.join( previewDirectory.path, - '${platform}_$releaseVersion.aab', + '${platform}_${release.version}.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( appId: appId, releaseId: release.id, - // TODO(felangel): add iOS support arch: 'aab', platform: platform, ); @@ -110,7 +162,7 @@ class PreviewCommand extends ShorebirdCommand final apksPath = p.join( previewDirectory.path, - '${platform}_$releaseVersion.apks', + '${platform}_${release.version}.apks', ); if (!File(apksPath).existsSync()) { @@ -153,26 +205,8 @@ class PreviewCommand extends ShorebirdCommand return process.exitCode; } - Future 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 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; + Future installAndLaunchIos(String appId, Release release) async { + return ExitCode.unavailable.code; } } diff --git a/packages/shorebird_cli/test/src/commands/preview_command_test.dart b/packages/shorebird_cli/test/src/commands/preview_command_test.dart index 03e1ab73..30bb7429 100644 --- a/packages/shorebird_cli/test/src/commands/preview_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/preview_command_test.dart @@ -130,6 +130,7 @@ void main() { when(() => argResults['app-id']).thenReturn(appId); when(() => argResults['release-version']).thenReturn(releaseVersion); + when(() => argResults['platform']).thenReturn(platform.name); when(() => auth.isAuthenticated).thenReturn(true); when(() => cache.getPreviewDirectory(any())).thenReturn(previewDirectory); when( @@ -138,12 +139,6 @@ void main() { 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( appId: any(named: 'appId'), @@ -156,6 +151,10 @@ void main() { when(() => app.displayName).thenReturn(appDisplayName); when(() => release.id).thenReturn(releaseId); when(() => release.version).thenReturn(releaseVersion); + when(() => release.platformStatuses).thenReturn({ + ReleasePlatform.android: ReleaseStatus.active, + ReleasePlatform.ios: ReleaseStatus.active, + }); when(() => releaseArtifact.url).thenReturn(releaseArtifactUrl); when(() => logger.progress(any())).thenReturn(progress); when( @@ -195,21 +194,17 @@ void main() { expect(result, ExitCode.noUser.code); }); - test('exits with code 70 when querying for release fails', () async { + test('exits with code 70 when querying for releases fails', () async { final exception = Exception('oops'); when( - () => codePushClientWrapper.getRelease( - appId: any(named: 'appId'), - releaseVersion: any(named: 'releaseVersion'), - ), + () => codePushClientWrapper.getReleases(appId: any(named: 'appId')), ).thenThrow(exception); - final result = await runWithOverrides(command.run); - expect(result, equals(ExitCode.software.code)); + await expectLater( + () => runWithOverrides(command.run), + throwsA(exception), + ); verify( - () => codePushClientWrapper.getRelease( - appId: appId, - releaseVersion: releaseVersion, - ), + () => codePushClientWrapper.getReleases(appId: appId), ).called(1); }); @@ -353,6 +348,39 @@ void main() { verify(() => codePushClientWrapper.getApps()).called(1); }); + test('prompts for platforms when platform is not specified', () async { + when(() => argResults['platform']).thenReturn(null); + when( + () => logger.chooseOne( + any(), + choices: any(named: 'choices'), + display: any(named: 'display'), + ), + ).thenReturn(platform.name); + final result = await runWithOverrides(command.run); + expect(result, equals(ExitCode.success.code)); + final platforms = verify( + () => logger.chooseOne( + any(), + choices: captureAny(named: 'choices'), + display: any(named: 'display'), + ), + ).captured.single as List; + expect( + platforms, + equals([ + ReleasePlatform.android.name, + ReleasePlatform.ios.name, + ]), + ); + }); + + test('exits with unavailable when platform is ios', () async { + when(() => argResults['platform']).thenReturn(ReleasePlatform.ios.name); + final result = await runWithOverrides(command.run); + expect(result, equals(ExitCode.unavailable.code)); + }); + test('exits early when no apps are found', () async { when(() => argResults['app-id']).thenReturn(null); when(() => codePushClientWrapper.getApps()).thenAnswer((_) async => []);