diff --git a/.github/workflows/very_good_cli.yaml b/.github/workflows/very_good_cli.yaml index 4a2ad3cb..145b8d0d 100644 --- a/.github/workflows/very_good_cli.yaml +++ b/.github/workflows/very_good_cli.yaml @@ -57,6 +57,7 @@ jobs: - test/src/commands/create/e2e/flutter_app/core_test.dart - test/src/commands/create/e2e/dart_package/dart_pkg_test.dart - test/src/commands/create/e2e/dart_cli/dart_cli_test.dart + - test/src/commands/create/e2e/docs_site/docs_site_test.dart # E2E tests for the legacy create command syntax - test/src/commands/create/e2e/legacy/core_test.dart diff --git a/lib/src/commands/create/commands/commands.dart b/lib/src/commands/create/commands/commands.dart index ab25dec2..145d9551 100644 --- a/lib/src/commands/create/commands/commands.dart +++ b/lib/src/commands/create/commands/commands.dart @@ -1,5 +1,6 @@ export 'create_subcommand.dart'; export 'dart_cli.dart'; export 'dart_package.dart'; +export 'docs_site.dart'; export 'flutter_app.dart'; export 'legacy.dart'; diff --git a/lib/src/commands/create/commands/docs_site.dart b/lib/src/commands/create/commands/docs_site.dart new file mode 100644 index 00000000..b404adcf --- /dev/null +++ b/lib/src/commands/create/commands/docs_site.dart @@ -0,0 +1,32 @@ +import 'package:mason_logger/mason_logger.dart'; +import 'package:usage/usage.dart'; +import 'package:very_good_cli/src/commands/commands.dart'; +import 'package:very_good_cli/src/commands/create/templates/templates.dart'; + +/// {@template very_good_create_docs_site} +/// A [CreateSubCommand] for creating Dart command line interfaces. +/// {@endtemplate} +class CreateDocsSite extends CreateSubCommand with OrgName { + /// {@macro very_good_create_docs_site} + CreateDocsSite({ + required Analytics analytics, + required Logger logger, + required MasonGeneratorFromBundle? generatorFromBundle, + required MasonGeneratorFromBrick? generatorFromBrick, + }) : super( + analytics: analytics, + logger: logger, + generatorFromBundle: generatorFromBundle, + generatorFromBrick: generatorFromBrick, + ); + + @override + String get name => 'docs_site'; + + @override + String get description => + 'Creates a new very good docs site in the specified directory.'; + + @override + Template get template => VeryGoodDocsSiteTemplate(); +} diff --git a/lib/src/commands/create/create.dart b/lib/src/commands/create/create.dart index cdee2f8f..7b0c9c85 100644 --- a/lib/src/commands/create/create.dart +++ b/lib/src/commands/create/create.dart @@ -58,6 +58,16 @@ class CreateCommand extends Command { generatorFromBrick: generatorFromBrick, ), ); + + // very_good create docs_site + addSubcommand( + CreateDocsSite( + analytics: analytics, + logger: logger, + generatorFromBundle: generatorFromBundle, + generatorFromBrick: generatorFromBrick, + ), + ); } @override diff --git a/test/src/commands/create/commands/docs_site_test.dart b/test/src/commands/create/commands/docs_site_test.dart new file mode 100644 index 00000000..e3e6f916 --- /dev/null +++ b/test/src/commands/create/commands/docs_site_test.dart @@ -0,0 +1,218 @@ +import 'dart:io'; + +import 'package:args/args.dart'; +import 'package:mason/mason.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:path/path.dart' as path; +import 'package:test/test.dart'; +import 'package:usage/usage.dart'; +import 'package:very_good_cli/src/commands/commands.dart'; + +import '../../../../helpers/helpers.dart'; + +class MockAnalytics extends Mock implements Analytics {} + +class MockLogger extends Mock implements Logger {} + +class MockMasonGenerator extends Mock implements MasonGenerator {} + +class MockGeneratorHooks extends Mock implements GeneratorHooks {} + +class MockArgResults extends Mock implements ArgResults {} + +class FakeLogger extends Fake implements Logger {} + +class FakeDirectoryGeneratorTarget extends Fake + implements DirectoryGeneratorTarget {} + +final expectedUsage = [ + ''' +Creates a new very good docs site in the specified directory. + +Usage: very_good create docs_site [arguments] +-h, --help Print this usage information. +-o, --output-directory The desired output directory when creating a new project. + --description The description for this new project. + (defaults to "A Very Good Project created by Very Good CLI.") + --org-name The organization for this new project. + (defaults to "com.example.verygoodcore") + +Run "very_good help" to see global options.''', +]; + +const pubspec = ''' +name: example +environment: + sdk: ">=2.13.0 <3.0.0" +'''; + +void main() { + late Analytics analytics; + late Logger logger; + + setUpAll(() { + registerFallbackValue(FakeDirectoryGeneratorTarget()); + registerFallbackValue(FakeLogger()); + }); + + setUp(() { + analytics = MockAnalytics(); + when( + () => analytics.sendEvent(any(), any(), label: any(named: 'label')), + ).thenAnswer((_) async {}); + when( + () => analytics.waitForLastPing(timeout: any(named: 'timeout')), + ).thenAnswer((_) async {}); + + logger = MockLogger(); + + final progress = MockProgress(); + + when(() => logger.progress(any())).thenReturn(progress); + }); + + group('can be instantiated', () { + test('with default options', () { + final logger = Logger(); + final command = CreateDocsSite( + analytics: analytics, + logger: logger, + generatorFromBundle: null, + generatorFromBrick: null, + ); + expect(command.name, equals('docs_site')); + expect( + command.description, + equals( + 'Creates a new very good docs site in the specified directory.', + ), + ); + expect(command.logger, equals(logger)); + expect(command, isA()); + }); + }); + + group('create docs_site', () { + test( + 'help', + withRunner((commandRunner, logger, pubUpdater, printLogs) async { + final result = + await commandRunner.run(['create', 'docs_site', '--help']); + expect(printLogs, equals(expectedUsage)); + expect(result, equals(ExitCode.success.code)); + + printLogs.clear(); + + final resultAbbr = + await commandRunner.run(['create', 'docs_site', '-h']); + expect(printLogs, equals(expectedUsage)); + expect(resultAbbr, equals(ExitCode.success.code)); + }), + ); + + group('running the command', () { + final generatedFiles = + List.filled(10, const GeneratedFile.created(path: '')); + + late GeneratorHooks hooks; + late MasonGenerator generator; + + setUp(() { + hooks = MockGeneratorHooks(); + generator = MockMasonGenerator(); + + when(() => generator.hooks).thenReturn(hooks); + when( + () => hooks.preGen( + vars: any(named: 'vars'), + onVarsChanged: any(named: 'onVarsChanged'), + ), + ).thenAnswer((_) async {}); + + when( + () => generator.generate( + any(), + vars: any(named: 'vars'), + logger: any(named: 'logger'), + ), + ).thenAnswer((_) async { + return generatedFiles; + }); + + when(() => generator.id).thenReturn('generator_id'); + when(() => generator.description).thenReturn('generator description'); + when(() => generator.hooks).thenReturn(hooks); + + when( + () => hooks.preGen( + vars: any(named: 'vars'), + onVarsChanged: any(named: 'onVarsChanged'), + ), + ).thenAnswer((_) async {}); + when( + () => generator.generate( + any(), + vars: any(named: 'vars'), + logger: any(named: 'logger'), + ), + ).thenAnswer((_) async { + final target = + _.positionalArguments.first as DirectoryGeneratorTarget; + File(path.join(target.dir.path, 'my_docs_site', 'pubspec.yaml')) + ..createSync(recursive: true) + ..writeAsStringSync(pubspec); + return generatedFiles; + }); + }); + + test('creates docs site', () async { + final tempDir = Directory.systemTemp.createTempSync(); + addTearDown(() => tempDir.deleteSync(recursive: true)); + final argResults = MockArgResults(); + final command = CreateDocsSite( + analytics: analytics, + logger: logger, + generatorFromBundle: (_) async => throw Exception('oops'), + generatorFromBrick: (_) async => generator, + )..argResultOverrides = argResults; + when(() => argResults['output-directory'] as String?) + .thenReturn(tempDir.path); + when(() => argResults.rest).thenReturn(['my_docs_site']); + when(() => argResults['org-name'] as String?).thenReturn( + 'xyz.app.my_app', + ); + + final result = await command.run(); + + expect(command.template.name, 'docs_site'); + expect(result, equals(ExitCode.success.code)); + + verify(() => logger.progress('Bootstrapping')).called(1); + verify( + () => hooks.preGen( + vars: { + 'project_name': 'my_docs_site', + 'description': '', + 'org_name': 'xyz.app.my_app', + }, + onVarsChanged: any(named: 'onVarsChanged'), + ), + ); + verify( + () => generator.generate( + any(), + vars: { + 'project_name': 'my_docs_site', + 'description': '', + 'org_name': 'xyz.app.my_app', + }, + logger: logger, + ), + ).called(1); + verify( + () => logger.info('Created a Very Good documentation site! 🦄'), + ).called(1); + }); + }); + }); +} diff --git a/test/src/commands/create/commands/legacy_test.dart b/test/src/commands/create/commands/legacy_test.dart index 388e6bdf..46b6a390 100644 --- a/test/src/commands/create/commands/legacy_test.dart +++ b/test/src/commands/create/commands/legacy_test.dart @@ -25,6 +25,7 @@ Usage: very_good create [arguments] Available subcommands: dart_cli Creates a new very good Dart CLI in the specified directory. dart_package Creates a new very good Dart package in the specified directory. + docs_site Creates a new very good docs site in the specified directory. flutter_app Creates a new very good Flutter app in the specified directory. Run "very_good help" to see global options.''' diff --git a/test/src/commands/create/create_test.dart b/test/src/commands/create/create_test.dart index 335292e2..f0859cd4 100644 --- a/test/src/commands/create/create_test.dart +++ b/test/src/commands/create/create_test.dart @@ -31,6 +31,7 @@ Usage: very_good create [arguments] Available subcommands: dart_cli Creates a new very good Dart CLI in the specified directory. dart_package Creates a new very good Dart package in the specified directory. + docs_site Creates a new very good docs site in the specified directory. flutter_app Creates a new very good Flutter app in the specified directory. Run "very_good help" to see global options.''' diff --git a/test/src/commands/create/e2e/docs_site/docs_site_test.dart b/test/src/commands/create/e2e/docs_site/docs_site_test.dart new file mode 100644 index 00000000..b4bc918a --- /dev/null +++ b/test/src/commands/create/e2e/docs_site/docs_site_test.dart @@ -0,0 +1,56 @@ +@Tags(['e2e']) +import 'package:mason/mason.dart'; +import 'package:path/path.dart' as path; +import 'package:test/test.dart'; +import 'package:universal_io/io.dart'; + +import '../../../../../helpers/helpers.dart'; + +void main() { + test( + 'create docs_site', + withRunner((commandRunner, logger, updater, logs) async { + final directory = Directory.systemTemp.createTempSync(); + final result = await commandRunner.run( + ['create', 'docs_site', 'very_good_docs_site', '-o', directory.path], + ); + expect(result, equals(ExitCode.success.code)); + + final installResult = await Process.run( + 'npm', + ['install'], + workingDirectory: path.join(directory.path, 'very_good_docs_site'), + runInShell: true, + ); + expect(installResult.exitCode, equals(ExitCode.success.code)); + + final formatResult = await Process.run( + 'npm', + ['run', 'format'], + workingDirectory: path.join(directory.path, 'very_good_docs_site'), + runInShell: true, + ); + expect(formatResult.exitCode, equals(ExitCode.success.code)); + expect(formatResult.stderr, isEmpty); + + final lintResult = await Process.run( + 'npm', + ['run', 'lint'], + workingDirectory: path.join(directory.path, 'very_good_docs_site'), + runInShell: true, + ); + expect(lintResult.exitCode, equals(ExitCode.success.code)); + expect(lintResult.stderr, isEmpty); + + final buildResult = await Process.run( + 'npm', + ['run', 'build'], + workingDirectory: path.join(directory.path, 'very_good_docs_site'), + runInShell: true, + ); + expect(buildResult.exitCode, equals(ExitCode.success.code)); + expect(buildResult.stderr, isEmpty); + }), + timeout: const Timeout(Duration(minutes: 2)), + ); +}