Files
michaelschiemer/tests/Framework/Console/ParsedArgumentsTest.php
Michael Schiemer 8f3c15ddbb 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
2025-11-10 11:06:07 +01:00

388 lines
12 KiB
PHP

<?php
declare(strict_types=1);
namespace Tests\Framework\Console;
use App\Framework\Console\ArgumentDefinition;
use App\Framework\Console\ArgumentType;
use App\Framework\Console\ParsedArguments;
describe('ParsedArguments', function () {
it('gets argument values', function () {
$definitions = [
'name' => new ArgumentDefinition('name', ArgumentType::STRING),
];
$parsed = new ParsedArguments(
['name' => 'John'],
[],
$definitions
);
expect($parsed->get('name'))->toBe('John');
});
it('gets option values', function () {
$definitions = [
'verbose' => new ArgumentDefinition('verbose', ArgumentType::BOOLEAN),
];
$parsed = new ParsedArguments(
[],
['verbose' => true],
$definitions
);
expect($parsed->get('verbose'))->toBeTrue();
});
it('returns default value when argument is missing', function () {
$definitions = [
'name' => new ArgumentDefinition('name', ArgumentType::STRING, default: 'Guest'),
];
$parsed = new ParsedArguments([], [], $definitions);
expect($parsed->get('name'))->toBe('Guest');
});
it('returns null when argument is missing and no default', function () {
$definitions = [
'name' => new ArgumentDefinition('name', ArgumentType::STRING),
];
$parsed = new ParsedArguments([], [], $definitions);
expect($parsed->get('name'))->toBeNull();
});
it('requires argument and throws exception when missing', function () {
$definitions = [
'name' => new ArgumentDefinition('name', ArgumentType::STRING, required: true),
];
$parsed = new ParsedArguments([], [], $definitions);
expect(fn () => $parsed->require('name'))
->toThrow(\InvalidArgumentException::class, "Required argument 'name' is missing");
});
it('requires argument and returns value when present', function () {
$definitions = [
'name' => new ArgumentDefinition('name', ArgumentType::STRING, required: true),
];
$parsed = new ParsedArguments(['name' => 'John'], [], $definitions);
expect($parsed->require('name'))->toBe('John');
});
it('gets typed values', function () {
$definitions = [
'count' => new ArgumentDefinition('count', ArgumentType::INTEGER),
'active' => new ArgumentDefinition('active', ArgumentType::BOOLEAN),
];
$parsed = new ParsedArguments(
['count' => '42', 'active' => 'true'],
[],
$definitions
);
expect($parsed->getTyped('count'))->toBe(42);
expect($parsed->getTyped('active'))->toBeTrue();
});
it('gets string value', function () {
$definitions = [
'name' => new ArgumentDefinition('name', ArgumentType::STRING),
];
$parsed = new ParsedArguments(['name' => 'John'], [], $definitions);
expect($parsed->getString('name'))->toBe('John');
expect($parsed->getString('name'))->toBeString();
});
it('gets integer value', function () {
$definitions = [
'count' => new ArgumentDefinition('count', ArgumentType::INTEGER),
];
$parsed = new ParsedArguments(['count' => '42'], [], $definitions);
expect($parsed->getInt('count'))->toBe(42);
expect($parsed->getInt('count'))->toBeInt();
});
it('throws exception for invalid integer', function () {
$definitions = [
'count' => new ArgumentDefinition('count', ArgumentType::INTEGER),
];
$parsed = new ParsedArguments(['count' => 'not-a-number'], [], $definitions);
expect(fn () => $parsed->getInt('count'))
->toThrow(\InvalidArgumentException::class, 'not a valid integer');
});
it('gets float value', function () {
$definitions = [
'price' => new ArgumentDefinition('price', ArgumentType::FLOAT),
];
$parsed = new ParsedArguments(['price' => '12.34'], [], $definitions);
expect($parsed->getFloat('price'))->toBe(12.34);
expect($parsed->getFloat('price'))->toBeFloat();
});
it('throws exception for invalid float', function () {
$definitions = [
'price' => new ArgumentDefinition('price', ArgumentType::FLOAT),
];
$parsed = new ParsedArguments(['price' => 'not-a-number'], [], $definitions);
expect(fn () => $parsed->getFloat('price'))
->toThrow(\InvalidArgumentException::class, 'not a valid number');
});
it('gets boolean value', function () {
$definitions = [
'active' => new ArgumentDefinition('active', ArgumentType::BOOLEAN),
];
$parsed = new ParsedArguments(['active' => 'true'], [], $definitions);
expect($parsed->getBool('active'))->toBeTrue();
});
it('parses various boolean string values', function () {
$definitions = [
'flag' => new ArgumentDefinition('flag', ArgumentType::BOOLEAN),
];
$trueValues = ['true', '1', 'yes', 'on'];
foreach ($trueValues as $value) {
$parsed = new ParsedArguments(['flag' => $value], [], $definitions);
expect($parsed->getBool('flag'))->toBeTrue();
}
});
it('gets array value', function () {
$definitions = [
'items' => new ArgumentDefinition('items', ArgumentType::ARRAY),
];
$parsed = new ParsedArguments(['items' => 'item1,item2,item3'], [], $definitions);
expect($parsed->getArray('items'))->toBe(['item1', 'item2', 'item3']);
});
it('handles array value that is already an array', function () {
$definitions = [
'items' => new ArgumentDefinition('items', ArgumentType::ARRAY),
];
$parsed = new ParsedArguments(['items' => ['item1', 'item2']], [], $definitions);
expect($parsed->getArray('items'))->toBe(['item1', 'item2']);
});
it('gets email value object', function () {
$definitions = [
'email' => new ArgumentDefinition('email', ArgumentType::EMAIL),
];
$parsed = new ParsedArguments(['email' => 'user@example.com'], [], $definitions);
$email = $parsed->getEmail('email');
expect($email)->toBeInstanceOf(\App\Framework\Core\ValueObjects\EmailAddress::class);
expect($email->toString())->toBe('user@example.com');
});
it('throws exception for invalid email', function () {
$definitions = [
'email' => new ArgumentDefinition('email', ArgumentType::EMAIL),
];
$parsed = new ParsedArguments(['email' => 'invalid-email'], [], $definitions);
expect(fn () => $parsed->getEmail('email'))
->toThrow(\InvalidArgumentException::class, 'not a valid email address');
});
it('gets URL value object', function () {
$definitions = [
'url' => new ArgumentDefinition('url', ArgumentType::URL),
];
$parsed = new ParsedArguments(['url' => 'https://example.com'], [], $definitions);
$url = $parsed->getUrl('url');
expect($url)->toBeInstanceOf(\App\Framework\Http\Url\Url::class);
});
it('throws exception for invalid URL', function () {
$definitions = [
'url' => new ArgumentDefinition('url', ArgumentType::URL),
];
$parsed = new ParsedArguments(['url' => 'not-a-url'], [], $definitions);
expect(fn () => $parsed->getUrl('url'))
->toThrow(\InvalidArgumentException::class, 'not a valid URL');
});
it('checks if argument exists', function () {
$definitions = [
'name' => new ArgumentDefinition('name', ArgumentType::STRING),
];
$parsed = new ParsedArguments(['name' => 'John'], [], $definitions);
expect($parsed->has('name'))->toBeTrue();
expect($parsed->has('missing'))->toBeFalse();
});
it('checks if argument has value', function () {
$definitions = [
'name' => new ArgumentDefinition('name', ArgumentType::STRING),
'empty' => new ArgumentDefinition('empty', ArgumentType::STRING),
];
$parsed = new ParsedArguments(
['name' => 'John', 'empty' => ''],
[],
$definitions
);
expect($parsed->hasValue('name'))->toBeTrue();
expect($parsed->hasValue('empty'))->toBeFalse();
expect($parsed->hasValue('missing'))->toBeFalse();
});
it('validates required arguments', function () {
$definitions = [
'name' => new ArgumentDefinition('name', ArgumentType::STRING, required: true),
'optional' => new ArgumentDefinition('optional', ArgumentType::STRING),
];
$parsed = new ParsedArguments(['name' => 'John'], [], $definitions);
// Should not throw
$parsed->validate();
expect(true)->toBeTrue();
});
it('throws exception when required argument is missing', function () {
$definitions = [
'name' => new ArgumentDefinition('name', ArgumentType::STRING, required: true),
];
$parsed = new ParsedArguments([], [], $definitions);
expect(fn () => $parsed->validate())
->toThrow(\InvalidArgumentException::class, "Required argument 'name' is missing");
});
it('validates allowed values', function () {
$definitions = [
'mode' => new ArgumentDefinition('mode', ArgumentType::STRING, allowedValues: ['dev', 'prod', 'test']),
];
$parsed = new ParsedArguments(['mode' => 'dev'], [], $definitions);
// Should not throw
$parsed->validate();
expect(true)->toBeTrue();
});
it('throws exception for invalid allowed value', function () {
$definitions = [
'mode' => new ArgumentDefinition('mode', ArgumentType::STRING, allowedValues: ['dev', 'prod', 'test']),
];
$parsed = new ParsedArguments(['mode' => 'invalid'], [], $definitions);
expect(fn () => $parsed->validate())
->toThrow(\InvalidArgumentException::class, "Invalid value 'invalid'");
});
it('returns all arguments', function () {
$parsed = new ParsedArguments(
['arg1' => 'value1', 'arg2' => 'value2'],
[],
[]
);
$all = $parsed->getAllArguments();
expect($all)->toHaveKey('arg1');
expect($all)->toHaveKey('arg2');
expect($all['arg1'])->toBe('value1');
});
it('returns all options', function () {
$parsed = new ParsedArguments(
[],
['opt1' => 'value1', 'opt2' => 'value2'],
[]
);
$all = $parsed->getAllOptions();
expect($all)->toHaveKey('opt1');
expect($all)->toHaveKey('opt2');
expect($all['opt1'])->toBe('value1');
});
it('returns all definitions', function () {
$definitions = [
'name' => new ArgumentDefinition('name', ArgumentType::STRING),
'count' => new ArgumentDefinition('count', ArgumentType::INTEGER),
];
$parsed = new ParsedArguments([], [], $definitions);
$all = $parsed->getDefinitions();
expect($all)->toHaveKey('name');
expect($all)->toHaveKey('count');
});
it('handles empty string as missing value', function () {
$definitions = [
'name' => new ArgumentDefinition('name', ArgumentType::STRING, required: true),
];
$parsed = new ParsedArguments(['name' => ''], [], $definitions);
expect(fn () => $parsed->validate())
->toThrow(\InvalidArgumentException::class, "Required argument 'name' is missing");
});
it('handles boolean false correctly', function () {
$definitions = [
'active' => new ArgumentDefinition('active', ArgumentType::BOOLEAN),
];
$parsed = new ParsedArguments(['active' => false], [], $definitions);
expect($parsed->getBool('active'))->toBeFalse();
expect($parsed->hasValue('active'))->toBeTrue();
});
it('handles empty array correctly', function () {
$definitions = [
'items' => new ArgumentDefinition('items', ArgumentType::ARRAY),
];
$parsed = new ParsedArguments(['items' => []], [], $definitions);
expect($parsed->getArray('items'))->toBe([]);
// Empty array is considered a value (it's not null or empty string)
expect($parsed->hasValue('items'))->toBeTrue();
});
});