Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 140 additions & 4 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,10 @@ public function execute(): int

## Testing Your Commands

The CLI component provides comprehensive testing support, including the ability to test commands that require user input.

### Testing Commands Without User Input

```php
use PHPUnit\Framework\TestCase;
use Neuron\Cli\Console\Input;
Expand All @@ -484,23 +488,155 @@ class MakeControllerCommandTest extends TestCase
public function testExecute(): void
{
$command = new MakeControllerCommand();

// Mock input
$input = new Input(['UserController', '--resource']);
$output = new Output(false); // No colors for testing

$command->setInput($input);
$command->setOutput($output);
$command->configure();
$input->parse($command);

$exitCode = $command->execute();

$this->assertEquals(0, $exitCode);
}
}
```

### Testing Commands With User Input

Commands that use `prompt()`, `confirm()`, `secret()`, or `choice()` can be tested using the `TestInputReader`:

```php
use PHPUnit\Framework\TestCase;
use Neuron\Cli\Console\Input;
use Neuron\Cli\Console\Output;
use Neuron\Cli\IO\TestInputReader;

class SetupCommandTest extends TestCase
{
public function testInteractiveSetup(): void
{
$command = new SetupCommand();

// Create test input reader with pre-programmed responses
$inputReader = new TestInputReader();
$inputReader->addResponse('my-project'); // Project name
$inputReader->addResponse('John Doe'); // Author name
$inputReader->addResponse('john@example.com'); // Email
$inputReader->addResponse('yes'); // Confirmation

// Configure command
$input = new Input([]);
$output = new Output(false);

$command->setInput($input);
$command->setOutput($output);
$command->setInputReader($inputReader);

// Execute command
$exitCode = $command->execute();

// Assertions
$this->assertEquals(0, $exitCode);

// Verify the prompts that were shown
$prompts = $inputReader->getPromptHistory();
$this->assertCount(4, $prompts);
$this->assertStringContainsString('Project name', $prompts[0]);
$this->assertStringContainsString('Author name', $prompts[1]);
}

public function testUserCancelsSetup(): void
{
$command = new SetupCommand();

// User will cancel the setup
$inputReader = new TestInputReader();
$inputReader->addResponses([
'test-project',
'Test User',
'test@example.com',
'no' // Cancel confirmation
]);

$input = new Input([]);
$output = new Output(false);

$command->setInput($input);
$command->setOutput($output);
$command->setInputReader($inputReader);

$exitCode = $command->execute();

// Should return non-zero exit code when cancelled
$this->assertNotEquals(0, $exitCode);
}
}
```

### Using Input Reader in Commands

To make your commands testable, use the convenience methods provided by the `Command` base class:

```php
class SetupCommand extends Command
{
public function execute(): int
{
// Use built-in convenience methods instead of reading STDIN directly
$name = $this->prompt('Enter project name: ');

if ($this->confirm('Enable caching?', true)) {
// User confirmed
}

$password = $this->secret('Enter password: ');

$env = $this->choice(
'Select environment:',
['development', 'staging', 'production'],
'development'
);

return 0;
}
}
```

These convenience methods automatically use the injected `IInputReader`, making your commands fully testable without requiring actual user input.

### TestInputReader Features

The `TestInputReader` class provides:

- **Response Queue**: Pre-program multiple responses with `addResponse()` or `addResponses()`
- **Prompt History**: Track all prompts shown with `getPromptHistory()`
- **Automatic Validation**: Throws exceptions if responses run out, helping catch test bugs
- **Fluent Interface**: Chain `addResponse()` calls for cleaner test setup
- **Full Interface Support**: Implements all `IInputReader` methods (prompt, confirm, secret, choice)

```php
$reader = new TestInputReader();
$reader
->addResponse('value1')
->addResponse('value2')
->addResponse('yes');

// Check if there are responses remaining
if ($reader->hasMoreResponses()) {
$count = $reader->getRemainingResponseCount();
}

// Get history of prompts
$prompts = $reader->getPromptHistory();

// Reset for reuse
$reader->reset();
```

## Contributing

When adding new features to the CLI component:
Expand Down
129 changes: 128 additions & 1 deletion src/Cli/Commands/Command.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

use Neuron\Cli\Console\Input;
use Neuron\Cli\Console\Output;
use Neuron\Cli\IO\IInputReader;
use Neuron\Cli\IO\StdinInputReader;

/**
* Abstract base class for all CLI commands.
Expand All @@ -14,6 +16,7 @@ abstract class Command
{
protected Input $input;
protected Output $output;
protected ?IInputReader $inputReader = null;
protected array $arguments = [];
protected array $options = [];

Expand Down Expand Up @@ -287,7 +290,7 @@ public function hasOption( string $name ): bool

/**
* Validate that required arguments are present
*
*
* @throws \InvalidArgumentException
* @return void
*/
Expand All @@ -301,4 +304,128 @@ public function validate(): void
}
}
}

/**
* Get the input reader instance.
*
* Creates a default StdinInputReader if not already set.
* This enables testable CLI commands by abstracting user input.
*
* If output hasn't been set, a default Output instance will be created automatically.
*
* @return IInputReader
*/
protected function getInputReader(): IInputReader
{
if( !$this->inputReader ) {
// Ensure output is initialized before creating StdinInputReader
if( !isset( $this->output ) ) {
$this->output = new Output();
}

$this->inputReader = new StdinInputReader( $this->output );
}

return $this->inputReader;
}

/**
* Set the input reader (for dependency injection, especially in tests).
*
* This allows tests to inject a TestInputReader with pre-programmed
* responses instead of requiring actual user input.
*
* @param IInputReader $inputReader
* @return self
*/
public function setInputReader( IInputReader $inputReader ): self
{
$this->inputReader = $inputReader;
return $this;
}

/**
* Prompt user for input.
*
* Convenience method that delegates to the input reader.
*
* Example:
* ```php
* $name = $this->prompt( "Enter your name: " );
* ```
*
* @param string $message The prompt message to display
* @return string The user's response (trimmed)
*/
protected function prompt( string $message ): string
{
return $this->getInputReader()->prompt( $message );
}

/**
* Ask user for yes/no confirmation.
*
* Convenience method that delegates to the input reader.
* Accepts: y, yes, true, 1 (case-insensitive) as positive responses.
*
* Example:
* ```php
* if( $this->confirm( "Delete all files?" ) ) {
* // User confirmed
* }
* ```
*
* @param string $message The confirmation message
* @param bool $default Default value if user just presses enter
* @return bool True if user confirms, false otherwise
*/
protected function confirm( string $message, bool $default = false ): bool
{
return $this->getInputReader()->confirm( $message, $default );
}

/**
* Prompt for sensitive input without echoing to console.
*
* Convenience method that delegates to the input reader.
* Note: Secret input hiding only works on Unix-like systems.
*
* Example:
* ```php
* $password = $this->secret( "Enter password: " );
* ```
*
* @param string $message The prompt message
* @return string The user's input (trimmed)
*/
protected function secret( string $message ): string
{
return $this->getInputReader()->secret( $message );
}

/**
* Prompt user to select from a list of options.
*
* Convenience method that delegates to the input reader.
* Users can select by entering either the option index (numeric)
* or the exact option text.
*
* Example:
* ```php
* $env = $this->choice(
* "Select environment:",
* ['development', 'staging', 'production'],
* 'development'
* );
* ```
*
* @param string $message The prompt message
* @param array<string> $options Available options
* @param string|null $default Default option (will be marked with *)
* @return string The selected option
*/
protected function choice( string $message, array $options, ?string $default = null ): string
{
return $this->getInputReader()->choice( $message, $options, $default );
}
}
59 changes: 59 additions & 0 deletions src/Cli/IO/IInputReader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

namespace Neuron\Cli\IO;

/**
* Interface for reading user input in CLI commands.
*
* Provides an abstraction over STDIN to enable testable CLI commands.
* Implementations can read from actual user input (StdinInputReader)
* or from pre-programmed responses (TestInputReader) for testing.
*
* @package Neuron\Cli\IO
*/
interface IInputReader
{
/**
* Prompt user for input and return their response.
*
* @param string $message The prompt message to display
* @return string The user's response (trimmed)
*/
public function prompt( string $message ): string;

/**
* Ask user for yes/no confirmation.
*
* Accepts: y, yes, true, 1 (case-insensitive) as positive responses.
* All other inputs are treated as negative responses.
*
* @param string $message The confirmation message
* @param bool $default Default value if user just presses enter
* @return bool True if user confirms, false otherwise
*/
public function confirm( string $message, bool $default = false ): bool;

/**
* Prompt for sensitive input without echoing to console.
*
* Note: Secret input is only supported on Unix-like systems.
* On Windows, input will be visible.
*
* @param string $message The prompt message
* @return string The user's input (trimmed)
*/
public function secret( string $message ): string;

/**
* Prompt user to select from a list of options.
*
* Users can select by entering either the option index (numeric)
* or the exact option text.
*
* @param string $message The prompt message
* @param array<string> $options Available options
* @param string|null $default Default option (will be marked with *)
* @return string The selected option
*/
public function choice( string $message, array $options, ?string $default = null ): string;
}
Loading