Files
michaelschiemer/tests/Framework/Console/ArgumentParserTest.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

284 lines
8.8 KiB
PHP

<?php
declare(strict_types=1);
namespace Tests\Framework\Console;
use App\Framework\Console\ArgumentDefinition;
use App\Framework\Console\ArgumentParser;
use App\Framework\Console\ArgumentParserBuilder;
use App\Framework\Console\ArgumentType;
describe('ArgumentParser', function () {
it('parses simple positional arguments', function () {
$parser = new ArgumentParser();
$parsed = $parser->parse(['arg1', 'arg2']);
expect($parsed->getAllArguments())->toBeArray();
});
it('parses long options with equals sign', function () {
$parser = ArgumentParser::create()
->optionalString('name')
->build();
$parsed = $parser->parse(['--name=John']);
expect($parsed->get('name'))->toBe('John');
});
it('parses long options with space', function () {
$parser = ArgumentParser::create()
->optionalString('name')
->build();
$parsed = $parser->parse(['--name', 'John']);
expect($parsed->get('name'))->toBe('John');
});
it('parses boolean flags', function () {
$parser = ArgumentParser::create()
->flag('verbose', 'v')
->build();
$parsed = $parser->parse(['--verbose']);
expect($parsed->getBool('verbose'))->toBeTrue();
});
it('parses short options', function () {
$parser = ArgumentParser::create()
->addArgument(new ArgumentDefinition('name', ArgumentType::STRING, shortName: 'n'))
->build();
$parsed = $parser->parse(['-n', 'John']);
expect($parsed->get('name'))->toBe('John');
});
it('parses combined short flags', function () {
$parser = ArgumentParser::create()
->flag('verbose', 'v')
->flag('force', 'f')
->build();
$parsed = $parser->parse(['-vf']);
expect($parsed->getBool('verbose'))->toBeTrue();
expect($parsed->getBool('force'))->toBeTrue();
});
it('converts kebab-case to camelCase', function () {
$parser = ArgumentParser::create()
->addArgument(new ArgumentDefinition('dryRun', ArgumentType::BOOLEAN))
->optionalString('outputFile')
->build();
$parsed = $parser->parse(['--dry-run', '--output-file=test.txt']);
expect($parsed->getBool('dryRun'))->toBeTrue();
expect($parsed->get('outputFile'))->toBe('test.txt');
});
it('validates required arguments', function () {
$parser = ArgumentParser::create()
->requiredString('name')
->build();
expect(fn () => $parser->parse([]))
->toThrow(\InvalidArgumentException::class, "Required argument 'name' is missing");
});
it('applies default values for optional arguments', function () {
$parser = ArgumentParser::create()
->optionalString('name', 'Guest')
->build();
$parsed = $parser->parse([]);
expect($parsed->get('name'))->toBe('Guest');
});
it('parses integer values', function () {
$parser = ArgumentParser::create()
->integer('count')
->build();
$parsed = $parser->parse(['--count=42']);
expect($parsed->getInt('count'))->toBe(42);
});
it('validates integer values', function () {
$parser = ArgumentParser::create()
->integer('count')
->build();
expect(fn () => $parser->parse(['--count=not-a-number']))
->toThrow(\InvalidArgumentException::class, 'not a valid integer');
});
it('parses float values', function () {
$parser = ArgumentParser::create()
->addArgument(new ArgumentDefinition('price', ArgumentType::FLOAT))
->build();
$parsed = $parser->parse(['--price=12.34']);
expect($parsed->getFloat('price'))->toBe(12.34);
});
it('parses boolean values', function () {
$parser = ArgumentParser::create()
->addArgument(new ArgumentDefinition('active', ArgumentType::BOOLEAN))
->build();
$parsed = $parser->parse(['--active=true']);
expect($parsed->getBool('active'))->toBeTrue();
});
it('parses array values from comma-separated string', function () {
$parser = ArgumentParser::create()
->addArgument(new ArgumentDefinition('items', ArgumentType::ARRAY))
->build();
$parsed = $parser->parse(['--items=item1,item2,item3']);
expect($parsed->getArray('items'))->toBe(['item1', 'item2', 'item3']);
});
it('validates email addresses', function () {
$parser = ArgumentParser::create()
->email('email')
->build();
$parsed = $parser->parse(['--email=user@example.com']);
expect($parsed->get('email'))->toBe('user@example.com');
});
it('throws exception for invalid email', function () {
$parser = ArgumentParser::create()
->email('email')
->build();
expect(fn () => $parser->parse(['--email=invalid-email']))
->toThrow(\InvalidArgumentException::class, 'not a valid email address');
});
it('validates URL addresses', function () {
$parser = ArgumentParser::create()
->addArgument(new ArgumentDefinition('url', ArgumentType::URL))
->build();
$parsed = $parser->parse(['--url=https://example.com']);
expect($parsed->get('url'))->toBe('https://example.com');
});
it('throws exception for invalid URL', function () {
$parser = ArgumentParser::create()
->addArgument(new ArgumentDefinition('url', ArgumentType::URL))
->build();
expect(fn () => $parser->parse(['--url=not-a-url']))
->toThrow(\InvalidArgumentException::class, 'not a valid URL');
});
it('validates allowed values', function () {
$parser = ArgumentParser::create()
->choice('mode', ['dev', 'prod', 'test'])
->build();
$parsed = $parser->parse(['--mode=dev']);
expect($parsed->get('mode'))->toBe('dev');
});
it('throws exception for invalid choice value', function () {
$parser = ArgumentParser::create()
->choice('mode', ['dev', 'prod', 'test'])
->build();
expect(fn () => $parser->parse(['--mode=invalid']))
->toThrow(\InvalidArgumentException::class, "Invalid value 'invalid'");
});
it('handles positional arguments', function () {
$parser = ArgumentParser::create()
->addArgument(new ArgumentDefinition('name', ArgumentType::STRING))
->addArgument(new ArgumentDefinition('age', ArgumentType::INTEGER))
->build();
$parsed = $parser->parse(['John', '25']);
expect($parsed->get('name'))->toBe('John');
expect($parsed->getInt('age'))->toBe(25);
});
it('throws exception when required option value is missing', function () {
$parser = ArgumentParser::create()
->requiredString('name')
->build();
expect(fn () => $parser->parse(['--name']))
->toThrow(\InvalidArgumentException::class, "Option '--name' requires a value");
});
it('handles optional boolean flags without value', function () {
$parser = ArgumentParser::create()
->flag('verbose', 'v')
->build();
$parsed = $parser->parse(['--verbose']);
expect($parsed->getBool('verbose'))->toBeTrue();
});
it('prevents combining short options that require values', function () {
$parser = ArgumentParser::create()
->addArgument(new ArgumentDefinition('name', ArgumentType::STRING, shortName: 'n'))
->flag('verbose', 'v')
->build();
expect(fn () => $parser->parse(['-nv']))
->toThrow(\InvalidArgumentException::class, "requires a value and cannot be combined");
});
it('returns all definitions', function () {
$parser = ArgumentParser::create()
->optionalString('name')
->flag('verbose', 'v')
->build();
$definitions = $parser->getDefinitions();
expect($definitions)->toHaveKey('name');
expect($definitions)->toHaveKey('verbose');
});
});
describe('ArgumentParserBuilder', function () {
it('creates parser with fluent interface', function () {
$parser = ArgumentParser::create()
->requiredString('name')
->build();
expect($parser)->toBeInstanceOf(ArgumentParser::class);
});
it('supports all argument definition factory methods', function () {
$parser = ArgumentParser::create()
->requiredString('arg1')
->optionalString('opt1')
->flag('flag1', 'f')
->choice('choice1', ['a', 'b', 'c'])
->build();
expect($parser)->toBeInstanceOf(ArgumentParser::class);
});
});