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:
237
tests/Framework/Console/CommandParameterResolverTest.php
Normal file
237
tests/Framework/Console/CommandParameterResolverTest.php
Normal file
@@ -0,0 +1,237 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Framework\Console;
|
||||
|
||||
use App\Framework\Console\CommandParameterResolver;
|
||||
use App\Framework\Console\ConsoleInput;
|
||||
use App\Framework\Console\ConsoleOutput;
|
||||
use App\Framework\Console\MethodSignatureAnalyzer;
|
||||
use ReflectionMethod;
|
||||
|
||||
class TestCommandClass
|
||||
{
|
||||
public function simpleMethod(): void
|
||||
{
|
||||
}
|
||||
|
||||
public function methodWithString(string $name): void
|
||||
{
|
||||
}
|
||||
|
||||
public function methodWithInt(int $count): void
|
||||
{
|
||||
}
|
||||
|
||||
public function methodWithBool(bool $active): void
|
||||
{
|
||||
}
|
||||
|
||||
public function methodWithArray(array $items): void
|
||||
{
|
||||
}
|
||||
|
||||
public function methodWithDefault(string $name = 'Guest'): void
|
||||
{
|
||||
}
|
||||
|
||||
public function methodWithNullable(?string $name): void
|
||||
{
|
||||
}
|
||||
|
||||
public function methodWithConsoleInput(ConsoleInput $input): void
|
||||
{
|
||||
}
|
||||
|
||||
public function methodWithConsoleOutput(ConsoleOutput $output): void
|
||||
{
|
||||
}
|
||||
|
||||
public function methodWithBoth(ConsoleInput $input, ConsoleOutput $output, string $name): void
|
||||
{
|
||||
}
|
||||
|
||||
public function methodWithOptionalFrameworkParams(string $name, ?ConsoleInput $input = null): void
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
describe('CommandParameterResolver', function () {
|
||||
beforeEach(function () {
|
||||
$this->analyzer = new MethodSignatureAnalyzer();
|
||||
$this->resolver = new CommandParameterResolver($this->analyzer);
|
||||
});
|
||||
|
||||
it('resolves simple string parameter', function () {
|
||||
$method = new ReflectionMethod(TestCommandClass::class, 'methodWithString');
|
||||
$resolved = $this->resolver->resolveParameters($method, ['--name=John']);
|
||||
|
||||
expect($resolved)->toHaveCount(1);
|
||||
expect($resolved[0])->toBe('John');
|
||||
});
|
||||
|
||||
it('resolves integer parameter', function () {
|
||||
$method = new ReflectionMethod(TestCommandClass::class, 'methodWithInt');
|
||||
$resolved = $this->resolver->resolveParameters($method, ['--count=42']);
|
||||
|
||||
expect($resolved)->toHaveCount(1);
|
||||
expect($resolved[0])->toBe(42);
|
||||
expect($resolved[0])->toBeInt();
|
||||
});
|
||||
|
||||
it('resolves boolean parameter', function () {
|
||||
$method = new ReflectionMethod(TestCommandClass::class, 'methodWithBool');
|
||||
$resolved = $this->resolver->resolveParameters($method, ['--active=true']);
|
||||
|
||||
expect($resolved)->toHaveCount(1);
|
||||
expect($resolved[0])->toBeTrue();
|
||||
});
|
||||
|
||||
it('resolves array parameter', function () {
|
||||
$method = new ReflectionMethod(TestCommandClass::class, 'methodWithArray');
|
||||
$resolved = $this->resolver->resolveParameters($method, ['--items=item1,item2,item3']);
|
||||
|
||||
expect($resolved)->toHaveCount(1);
|
||||
expect($resolved[0])->toBe(['item1', 'item2', 'item3']);
|
||||
});
|
||||
|
||||
it('uses default values for optional parameters', function () {
|
||||
$method = new ReflectionMethod(TestCommandClass::class, 'methodWithDefault');
|
||||
$resolved = $this->resolver->resolveParameters($method, []);
|
||||
|
||||
expect($resolved)->toHaveCount(1);
|
||||
expect($resolved[0])->toBe('Guest');
|
||||
});
|
||||
|
||||
it('resolves nullable parameters as null when missing', function () {
|
||||
$method = new ReflectionMethod(TestCommandClass::class, 'methodWithNullable');
|
||||
$resolved = $this->resolver->resolveParameters($method, []);
|
||||
|
||||
expect($resolved)->toHaveCount(1);
|
||||
expect($resolved[0])->toBeNull();
|
||||
});
|
||||
|
||||
it('resolves ConsoleInput parameter', function () {
|
||||
$method = new ReflectionMethod(TestCommandClass::class, 'methodWithConsoleInput');
|
||||
$input = new ConsoleInput([]);
|
||||
$resolved = $this->resolver->resolveParameters($method, [], $input);
|
||||
|
||||
expect($resolved)->toHaveCount(1);
|
||||
expect($resolved[0])->toBe($input);
|
||||
});
|
||||
|
||||
it('resolves ConsoleOutput parameter', function () {
|
||||
$method = new ReflectionMethod(TestCommandClass::class, 'methodWithConsoleOutput');
|
||||
$output = new ConsoleOutput();
|
||||
$resolved = $this->resolver->resolveParameters($method, [], null, $output);
|
||||
|
||||
expect($resolved)->toHaveCount(1);
|
||||
expect($resolved[0])->toBe($output);
|
||||
});
|
||||
|
||||
it('resolves both ConsoleInput and ConsoleOutput', function () {
|
||||
$method = new ReflectionMethod(TestCommandClass::class, 'methodWithBoth');
|
||||
$input = new ConsoleInput([]);
|
||||
$output = new ConsoleOutput();
|
||||
$resolved = $this->resolver->resolveParameters($method, ['--name=John'], $input, $output);
|
||||
|
||||
expect($resolved)->toHaveCount(3);
|
||||
expect($resolved[0])->toBe($input);
|
||||
expect($resolved[1])->toBe($output);
|
||||
expect($resolved[2])->toBe('John');
|
||||
});
|
||||
|
||||
it('handles optional framework parameters', function () {
|
||||
$method = new ReflectionMethod(TestCommandClass::class, 'methodWithOptionalFrameworkParams');
|
||||
$resolved = $this->resolver->resolveParameters($method, ['--name=John']);
|
||||
|
||||
expect($resolved)->toHaveCount(2);
|
||||
expect($resolved[0])->toBe('John');
|
||||
expect($resolved[1])->toBeNull();
|
||||
});
|
||||
|
||||
it('throws exception for missing required parameter', function () {
|
||||
$method = new ReflectionMethod(TestCommandClass::class, 'methodWithString');
|
||||
|
||||
expect(fn () => $this->resolver->resolveParameters($method, []))
|
||||
->toThrow(\InvalidArgumentException::class, "Required parameter 'name' is missing");
|
||||
});
|
||||
|
||||
it('throws exception when ConsoleInput is required but not provided', function () {
|
||||
$method = new ReflectionMethod(TestCommandClass::class, 'methodWithConsoleInput');
|
||||
|
||||
expect(fn () => $this->resolver->resolveParameters($method, []))
|
||||
->toThrow(\InvalidArgumentException::class, "ConsoleInput is required but not provided");
|
||||
});
|
||||
|
||||
it('validates method signature compatibility', function () {
|
||||
$method = new ReflectionMethod(TestCommandClass::class, 'simpleMethod');
|
||||
|
||||
// Should not throw for valid method
|
||||
$this->resolver->validateMethodSignature($method);
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
|
||||
it('creates parser for method', function () {
|
||||
$method = new ReflectionMethod(TestCommandClass::class, 'methodWithString');
|
||||
$parser = $this->resolver->createParserForMethod($method);
|
||||
|
||||
expect($parser)->toBeInstanceOf(\App\Framework\Console\ArgumentParser::class);
|
||||
});
|
||||
});
|
||||
|
||||
describe('CommandParameterResolver Type Conversion', function () {
|
||||
beforeEach(function () {
|
||||
$this->analyzer = new MethodSignatureAnalyzer();
|
||||
$this->resolver = new CommandParameterResolver($this->analyzer);
|
||||
});
|
||||
|
||||
it('converts string values correctly', function () {
|
||||
$method = new ReflectionMethod(TestCommandClass::class, 'methodWithString');
|
||||
$resolved = $this->resolver->resolveParameters($method, ['--name=123']);
|
||||
|
||||
expect($resolved[0])->toBe('123');
|
||||
expect($resolved[0])->toBeString();
|
||||
});
|
||||
|
||||
it('converts integer values correctly', function () {
|
||||
$method = new ReflectionMethod(TestCommandClass::class, 'methodWithInt');
|
||||
$resolved = $this->resolver->resolveParameters($method, ['--count=42']);
|
||||
|
||||
expect($resolved[0])->toBe(42);
|
||||
expect($resolved[0])->toBeInt();
|
||||
});
|
||||
|
||||
it('converts boolean values correctly', function () {
|
||||
$method = new ReflectionMethod(TestCommandClass::class, 'methodWithBool');
|
||||
|
||||
$resolved = $this->resolver->resolveParameters($method, ['--active=true']);
|
||||
expect($resolved[0])->toBeTrue();
|
||||
|
||||
$resolved = $this->resolver->resolveParameters($method, ['--active=false']);
|
||||
expect($resolved[0])->toBeFalse();
|
||||
|
||||
$resolved = $this->resolver->resolveParameters($method, ['--active=1']);
|
||||
expect($resolved[0])->toBeTrue();
|
||||
|
||||
$resolved = $this->resolver->resolveParameters($method, ['--active=0']);
|
||||
expect($resolved[0])->toBeFalse();
|
||||
});
|
||||
|
||||
it('converts array values correctly', function () {
|
||||
$method = new ReflectionMethod(TestCommandClass::class, 'methodWithArray');
|
||||
$resolved = $this->resolver->resolveParameters($method, ['--items=item1,item2,item3']);
|
||||
|
||||
expect($resolved[0])->toBeArray();
|
||||
expect($resolved[0])->toBe(['item1', 'item2', 'item3']);
|
||||
});
|
||||
|
||||
it('throws exception for invalid type conversion', function () {
|
||||
$method = new ReflectionMethod(TestCommandClass::class, 'methodWithInt');
|
||||
|
||||
expect(fn () => $this->resolver->resolveParameters($method, ['--count=not-a-number']))
|
||||
->toThrow(\InvalidArgumentException::class);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user