- 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
284 lines
8.8 KiB
PHP
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);
|
|
});
|
|
});
|
|
|