From d259ce43989b213894a64ebcad01339215d986d2 Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Thu, 6 Oct 2022 17:37:26 -0400 Subject: [PATCH] [feature] add ability to test command completion (#15) --- README.md | 18 ++++++ phpstan.neon | 1 - src/Assert/CompletionExpectation.php | 41 +++++++++++++ src/TestCommand.php | 12 ++++ tests/UnitTest.php | 90 ++++++++++++++++++++++++++++ 5 files changed, 161 insertions(+), 1 deletion(-) create mode 100644 src/Assert/CompletionExpectation.php diff --git a/README.md b/README.md index 2d90520..582efc8 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,15 @@ class CreateUserCommandTest extends KernelTestCase ->assertOutputContains('Could not create user!') // can still make assertions on output before exception was thrown ; + // test completion + $this->consoleCommand('create:user') + ->complete('') + ->is(['kevin', 'john', 'jane']) + ->contains('kevin') // chain assertions + ->back() // fluently go back to the TestCommand + ->complete('kevin --role=')->is(['ROLE_EMPLOYEE', 'ROLE_MANAGER']) + ; + // access result $result = $this->executeConsoleCommand('create:user'); @@ -143,6 +152,15 @@ class CreateUserCommandTest extends TestCase ->assertOutputContains('Could not create user!') // can still make assertions on output before exception was thrown ; + // test completion + TestCommand::for(new CreateUserCommand(/** args... */)) + ->complete('') + ->is(['kevin', 'john', 'jane']) + ->contains('kevin') // chain assertions + ->back() // fluently go back to the TestCommand + ->complete('kevin --role=')->is(['ROLE_EMPLOYEE', 'ROLE_MANAGER']) + ; + // access result $result = TestCommand::for(new CreateUserCommand(/** args... */))->execute(); diff --git a/phpstan.neon b/phpstan.neon index 1cd333b..1ff472c 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -2,4 +2,3 @@ parameters: level: 8 paths: - src - - tests diff --git a/src/Assert/CompletionExpectation.php b/src/Assert/CompletionExpectation.php new file mode 100644 index 0000000..692a002 --- /dev/null +++ b/src/Assert/CompletionExpectation.php @@ -0,0 +1,41 @@ + + * + * @mixin Expectation + */ +final class CompletionExpectation +{ + private TestCommand $command; + private Expectation $expectation; + + /** + * @internal + */ + public function __construct(TestCommand $command, Expectation $expectation) + { + $this->command = $command; + $this->expectation = $expectation; + } + + /** + * @internal + */ + public function __call(string $name, array $arguments): self // @phpstan-ignore-line + { + $this->expectation->{$name}(...$arguments); + + return $this; + } + + public function back(): TestCommand + { + return $this->command; + } +} diff --git a/src/TestCommand.php b/src/TestCommand.php index 8f84a9c..f46beca 100644 --- a/src/TestCommand.php +++ b/src/TestCommand.php @@ -4,7 +4,9 @@ use Symfony\Component\Console\Application; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Tester\CommandCompletionTester; use Zenstruck\Assert; +use Zenstruck\Console\Test\Assert\CompletionExpectation; /** * @author Kevin Bond @@ -13,6 +15,7 @@ final class TestCommand { private Application $application; private string $cli; + private Command $command; /** @var string[] */ private array $inputs = []; @@ -33,6 +36,7 @@ private function __construct(Command $command, string $cli) $this->application = $application; $this->cli = $cli; + $this->command = $command; } public static function for(Command $command): self @@ -140,6 +144,14 @@ public function execute(?string $cli = null): CommandResult return new CommandResult($cli, $status, $output); } + public function complete(string $cli): CompletionExpectation + { + return new CompletionExpectation( + $this, + Assert::that((new CommandCompletionTester($this->command))->complete(\explode(' ', $cli))) + ); + } + private function doRun(TestInput $input, TestOutput $output): int { $fn = fn() => $this->application->run($input, $output); diff --git a/tests/UnitTest.php b/tests/UnitTest.php index cd4ac2c..2b15402 100644 --- a/tests/UnitTest.php +++ b/tests/UnitTest.php @@ -3,6 +3,11 @@ namespace Zenstruck\Console\Test\Tests; use PHPUnit\Framework\TestCase; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\HttpKernel\Kernel; use Zenstruck\Console\Test\TestCommand; use Zenstruck\Console\Test\Tests\Fixture\FixtureCommand; @@ -193,4 +198,89 @@ public function always_executes_with_console_output(): void ->assertOutputContains('table row 2') ; } + + /** + * @test + */ + public function completion(): void + { + if (!\class_exists(CompletionInput::class)) { + $this->markTestSkipped('Command completion not available.'); + } + + $command = TestCommand::for(new class() extends Command { + public function getName(): string + { + return 'my:command'; + } + + public function configure(): void + { + $this + ->addArgument('user') + ->addOption('message', null, InputOption::VALUE_REQUIRED) + ; + } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestArgumentValuesFor('user')) { + $suggestions->suggestValues(['kevin', 'john', 'jane']); + } + + if ($input->mustSuggestOptionValuesFor('message')) { + $suggestions->suggestValues(['hello', 'hi', 'greetings']); + } + } + }); + + $command + ->complete('') + ->is(['kevin', 'john', 'jane']) + ->contains('kevin') + ->back() + ->complete('ke')->is(['kevin', 'john', 'jane'])->back() + ->complete('kevin --message=')->is(['hello', 'hi', 'greetings'])->back() + ->complete('kevin --message=g')->is(['hello', 'hi', 'greetings'])->back() + ; + } + + /** + * @test + */ + public function completion_61(): void + { + if (!\class_exists(CompletionInput::class)) { + $this->markTestSkipped('Command completion not available.'); + } + + if (Kernel::VERSION_ID < 60100) { + $this->markTestSkipped('Using InputArgument/Option for defining suggestions requires 6.1+.'); + } + + $command = TestCommand::for(new class() extends Command { + public function getName(): string + { + return 'my:command'; + } + + public function configure(): void + { + $this + ->addArgument('user', null, '', null, ['kevin', 'john', 'jane']) + ->addOption('message', null, InputOption::VALUE_REQUIRED, '', null, ['hello', 'hi', 'greetings']) + ; + } + }); + + $command + ->complete('') + ->is(['kevin', 'john', 'jane']) + ->contains('kevin') + ->back() + ->complete('ke')->is(['kevin', 'john', 'jane'])->back() + ->complete('kevin --message=')->is(['hello', 'hi', 'greetings'])->back() + ->complete('kevin --message=g')->is(['hello', 'hi', 'greetings'])->back() + ; + } }