fix(console): comprehensive TUI rendering fixes
- Fix Enter key detection: handle multiple Enter key formats (\n, \r, \r\n) - Reduce flickering: lower render frequency from 60 FPS to 30 FPS - Fix menu bar visibility: re-render menu bar after content to prevent overwriting - Fix content positioning: explicit line positioning for categories and commands - Fix line shifting: clear lines before writing, control newlines manually - Limit visible items: prevent overflow with maxVisibleCategories/Commands - Improve CPU usage: increase sleep interval when no events processed This fixes: - Enter key not working for selection - Strong flickering of the application - Menu bar not visible or being overwritten - Top half of selection list not displayed - Lines being shifted/misaligned
This commit is contained in:
199
tests/Framework/Console/ConsoleInputTest.php
Normal file
199
tests/Framework/Console/ConsoleInputTest.php
Normal file
@@ -0,0 +1,199 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Framework\Console;
|
||||
|
||||
use App\Framework\Console\ArgumentParser;
|
||||
use App\Framework\Console\ArgumentParserBuilder;
|
||||
use App\Framework\Console\ArgumentType;
|
||||
use App\Framework\Console\ConsoleInput;
|
||||
use App\Framework\Console\ConsoleOutput;
|
||||
|
||||
describe('ConsoleInput', function () {
|
||||
beforeEach(function () {
|
||||
$this->output = new ConsoleOutput();
|
||||
});
|
||||
|
||||
it('parses simple arguments', function () {
|
||||
$input = new ConsoleInput(['arg1', 'arg2', 'arg3'], $this->output);
|
||||
|
||||
expect($input->getArgument(0))->toBe('arg1');
|
||||
expect($input->getArgument(1))->toBe('arg2');
|
||||
expect($input->getArgument(2))->toBe('arg3');
|
||||
});
|
||||
|
||||
it('returns default value for missing arguments', function () {
|
||||
$input = new ConsoleInput(['arg1'], $this->output);
|
||||
|
||||
expect($input->getArgument(0))->toBe('arg1');
|
||||
expect($input->getArgument(1, 'default'))->toBe('default');
|
||||
expect($input->getArgument(2))->toBeNull();
|
||||
});
|
||||
|
||||
it('parses long options with equals sign', function () {
|
||||
$input = new ConsoleInput(['--option=value', '--flag'], $this->output);
|
||||
|
||||
expect($input->getOption('option'))->toBe('value');
|
||||
expect($input->hasOption('flag'))->toBeTrue();
|
||||
expect($input->getOption('flag'))->toBeTrue();
|
||||
});
|
||||
|
||||
it('parses long options with space', function () {
|
||||
// Note: Simple parser doesn't support --option value, only --option=value
|
||||
// This test verifies the current behavior
|
||||
$input = new ConsoleInput(['--option', 'value'], $this->output);
|
||||
|
||||
// Simple parser treats 'value' as a separate argument, not as option value
|
||||
expect($input->hasOption('option'))->toBeTrue();
|
||||
expect($input->getArgument(0))->toBe('value');
|
||||
});
|
||||
|
||||
it('parses short options', function () {
|
||||
$input = new ConsoleInput(['-f', '-o', 'value'], $this->output);
|
||||
|
||||
expect($input->hasOption('f'))->toBeTrue();
|
||||
// Simple parser treats 'value' as argument, not as option value
|
||||
expect($input->getArgument(0))->toBe('value');
|
||||
});
|
||||
|
||||
it('parses mixed arguments and options', function () {
|
||||
$input = new ConsoleInput(['arg1', '--option=value', 'arg2', '-f'], $this->output);
|
||||
|
||||
expect($input->getArgument(0))->toBe('arg1');
|
||||
expect($input->getArgument(1))->toBe('arg2');
|
||||
expect($input->getOption('option'))->toBe('value');
|
||||
expect($input->hasOption('f'))->toBeTrue();
|
||||
});
|
||||
|
||||
it('returns all arguments', function () {
|
||||
$input = new ConsoleInput(['arg1', 'arg2', 'arg3'], $this->output);
|
||||
|
||||
$args = $input->getArguments();
|
||||
expect($args)->toBe(['arg1', 'arg2', 'arg3']);
|
||||
});
|
||||
|
||||
it('returns all options', function () {
|
||||
$input = new ConsoleInput(['--opt1=val1', '--opt2', '-f'], $this->output);
|
||||
|
||||
$options = $input->getOptions();
|
||||
expect($options)->toHaveKey('opt1');
|
||||
expect($options)->toHaveKey('opt2');
|
||||
expect($options)->toHaveKey('f');
|
||||
});
|
||||
|
||||
it('returns default value for missing options', function () {
|
||||
$input = new ConsoleInput([], $this->output);
|
||||
|
||||
expect($input->getOption('missing', 'default'))->toBe('default');
|
||||
expect($input->getOption('missing'))->toBeNull();
|
||||
});
|
||||
|
||||
it('supports enhanced parsing with ArgumentParser', function () {
|
||||
$parser = ArgumentParser::create()
|
||||
->requiredString('name')
|
||||
->integer('age', required: false, default: 18)
|
||||
->build();
|
||||
|
||||
$input = new ConsoleInput(['--name=John', '--age=25'], $this->output, $parser);
|
||||
|
||||
expect($input->hasEnhancedParsing())->toBeTrue();
|
||||
expect($input->getString('name'))->toBe('John');
|
||||
expect($input->getInt('age'))->toBe(25);
|
||||
});
|
||||
|
||||
it('throws exception when accessing enhanced parsing without parser', function () {
|
||||
$input = new ConsoleInput(['--option=value'], $this->output);
|
||||
|
||||
expect($input->hasEnhancedParsing())->toBeFalse();
|
||||
expect(fn () => $input->getParsedArguments())
|
||||
->toThrow(\RuntimeException::class, 'Enhanced parsing not available');
|
||||
});
|
||||
|
||||
it('validates required arguments with enhanced parsing', function () {
|
||||
$parser = ArgumentParser::create()
|
||||
->requiredString('name')
|
||||
->build();
|
||||
|
||||
$input = new ConsoleInput(['--name=John'], $this->output, $parser);
|
||||
|
||||
expect($input->require('name'))->toBe('John');
|
||||
});
|
||||
|
||||
it('throws exception for missing required arguments', function () {
|
||||
$parser = ArgumentParser::create()
|
||||
->requiredString('name')
|
||||
->build();
|
||||
|
||||
// Exception is thrown during parsing, not when calling require()
|
||||
expect(fn () => new ConsoleInput([], $this->output, $parser))
|
||||
->toThrow(\InvalidArgumentException::class, "Required argument 'name' is missing");
|
||||
});
|
||||
|
||||
it('converts types correctly with enhanced parsing', function () {
|
||||
$parser = ArgumentParser::create()
|
||||
->integer('count')
|
||||
->addArgument(new \App\Framework\Console\ArgumentDefinition('active', ArgumentType::BOOLEAN))
|
||||
->addArgument(new \App\Framework\Console\ArgumentDefinition('items', ArgumentType::ARRAY))
|
||||
->build();
|
||||
|
||||
$input = new ConsoleInput([
|
||||
'--count=42',
|
||||
'--active=true',
|
||||
'--items=item1,item2,item3'
|
||||
], $this->output, $parser);
|
||||
|
||||
expect($input->getInt('count'))->toBe(42);
|
||||
expect($input->getBool('active'))->toBeTrue();
|
||||
expect($input->getArray('items'))->toBe(['item1', 'item2', 'item3']);
|
||||
});
|
||||
|
||||
it('handles kebab-case option names', function () {
|
||||
$input = new ConsoleInput(['--dry-run', '--output-file=test.txt'], $this->output);
|
||||
|
||||
expect($input->hasOption('dry-run'))->toBeTrue();
|
||||
expect($input->getOption('output-file'))->toBe('test.txt');
|
||||
});
|
||||
|
||||
it('can set argument parser after construction', function () {
|
||||
$input = new ConsoleInput(['--name=John'], $this->output);
|
||||
|
||||
$parser = ArgumentParser::create()
|
||||
->optionalString('name')
|
||||
->build();
|
||||
|
||||
$input->setArgumentParser($parser);
|
||||
|
||||
expect($input->hasEnhancedParsing())->toBeTrue();
|
||||
// Note: setArgumentParser re-parses, but needs the raw arguments
|
||||
// The simple parser already parsed --name=John, so we can verify it's available
|
||||
expect($input->getOption('name'))->toBe('John');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ConsoleInput Interactive Methods', function () {
|
||||
it('has ask method that delegates to InteractivePrompter', function () {
|
||||
$output = new ConsoleOutput();
|
||||
$input = new ConsoleInput([], $output);
|
||||
|
||||
// Verify method exists - actual interactive behavior requires STDIN
|
||||
expect(method_exists($input, 'ask'))->toBeTrue();
|
||||
});
|
||||
|
||||
it('has confirm method that delegates to InteractivePrompter', function () {
|
||||
$output = new ConsoleOutput();
|
||||
$input = new ConsoleInput([], $output);
|
||||
|
||||
// Verify method exists - actual interactive behavior requires STDIN
|
||||
expect(method_exists($input, 'confirm'))->toBeTrue();
|
||||
});
|
||||
|
||||
it('has askPassword method that delegates to InteractivePrompter', function () {
|
||||
$output = new ConsoleOutput();
|
||||
$input = new ConsoleInput([], $output);
|
||||
|
||||
// Verify method exists - actual interactive behavior requires STDIN
|
||||
expect(method_exists($input, 'askPassword'))->toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user