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:
243
tests/Framework/Console/SpinnerTest.php
Normal file
243
tests/Framework/Console/SpinnerTest.php
Normal file
@@ -0,0 +1,243 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Framework\Console;
|
||||
|
||||
use App\Framework\Console\ConsoleColor;
|
||||
use App\Framework\Console\Spinner;
|
||||
use App\Framework\Console\SpinnerStyle;
|
||||
use Tests\Framework\Console\Helpers\TestConsoleOutput;
|
||||
|
||||
describe('Spinner', function () {
|
||||
beforeEach(function () {
|
||||
$this->output = new TestConsoleOutput();
|
||||
});
|
||||
|
||||
it('creates spinner with default values', function () {
|
||||
$spinner = new Spinner($this->output);
|
||||
|
||||
expect($spinner)->toBeInstanceOf(Spinner::class);
|
||||
});
|
||||
|
||||
it('creates spinner with custom message', function () {
|
||||
$spinner = new Spinner($this->output, 'Processing...');
|
||||
|
||||
expect($spinner)->toBeInstanceOf(Spinner::class);
|
||||
});
|
||||
|
||||
it('creates spinner with custom style', function () {
|
||||
$spinner = new Spinner($this->output, 'Loading...', SpinnerStyle::ARROW);
|
||||
|
||||
expect($spinner)->toBeInstanceOf(Spinner::class);
|
||||
});
|
||||
|
||||
it('creates spinner with custom interval', function () {
|
||||
$spinner = new Spinner($this->output, 'Loading...', SpinnerStyle::DOTS, 0.2);
|
||||
|
||||
expect($spinner)->toBeInstanceOf(Spinner::class);
|
||||
});
|
||||
|
||||
it('starts spinner', function () {
|
||||
$spinner = new Spinner($this->output, 'Loading...');
|
||||
$spinner->start();
|
||||
|
||||
// Spinner uses write() which is captured in capturedWrites
|
||||
// But it may not write immediately if interval hasn't passed
|
||||
expect($this->output->capturedWrites)->toBeArray();
|
||||
});
|
||||
|
||||
it('updates spinner frames', function () {
|
||||
$spinner = new Spinner($this->output, 'Loading...', \App\Framework\Console\SpinnerStyle::DOTS, 0.01);
|
||||
$spinner->start();
|
||||
usleep(20000); // Wait 20ms to trigger update
|
||||
$spinner->update();
|
||||
|
||||
// Spinner may write if enough time has passed
|
||||
expect($this->output->capturedWrites)->toBeArray();
|
||||
});
|
||||
|
||||
it('stops spinner without message', function () {
|
||||
$spinner = new Spinner($this->output, 'Loading...');
|
||||
$spinner->start();
|
||||
$spinner->stop();
|
||||
|
||||
expect($this->output->capturedWrites)->not->toBeEmpty();
|
||||
});
|
||||
|
||||
it('stops spinner with message', function () {
|
||||
$spinner = new Spinner($this->output, 'Loading...');
|
||||
$spinner->start();
|
||||
$spinner->stop('Done!');
|
||||
|
||||
expect($this->output->capturedLines)->not->toBeEmpty();
|
||||
expect($this->output->getLastLine())->toContain('Done!');
|
||||
});
|
||||
|
||||
it('stops spinner with message and color', function () {
|
||||
$spinner = new Spinner($this->output, 'Loading...');
|
||||
$spinner->start();
|
||||
$spinner->stop('Done!', ConsoleColor::GREEN);
|
||||
|
||||
expect($this->output->capturedLines)->not->toBeEmpty();
|
||||
});
|
||||
|
||||
it('stops spinner with success message', function () {
|
||||
$spinner = new Spinner($this->output, 'Loading...');
|
||||
$spinner->start();
|
||||
$spinner->success('Completed successfully!');
|
||||
|
||||
expect($this->output->capturedLines)->not->toBeEmpty();
|
||||
$output = $this->output->getOutput();
|
||||
expect($output)->toContain('✓');
|
||||
expect($output)->toContain('Completed successfully!');
|
||||
});
|
||||
|
||||
it('stops spinner with error message', function () {
|
||||
$spinner = new Spinner($this->output, 'Loading...');
|
||||
$spinner->start();
|
||||
$spinner->error('Failed to complete!');
|
||||
|
||||
expect($this->output->capturedLines)->not->toBeEmpty();
|
||||
$output = $this->output->getOutput();
|
||||
expect($output)->toContain('✗');
|
||||
expect($output)->toContain('Failed to complete!');
|
||||
});
|
||||
|
||||
it('sets message while running', function () {
|
||||
$spinner = new Spinner($this->output, 'Loading...');
|
||||
$spinner->start();
|
||||
$spinner->setMessage('Processing...');
|
||||
|
||||
// setMessage calls update() which may write if interval passed
|
||||
expect($this->output->capturedWrites)->toBeArray();
|
||||
});
|
||||
|
||||
it('does not update when not active', function () {
|
||||
$spinner = new Spinner($this->output, 'Loading...');
|
||||
$spinner->update();
|
||||
|
||||
// Should not write anything when not active
|
||||
expect($this->output->capturedWrites)->toBeEmpty();
|
||||
});
|
||||
|
||||
it('clears spinner', function () {
|
||||
$spinner = new Spinner($this->output, 'Loading...');
|
||||
$spinner->start();
|
||||
$spinner->clear();
|
||||
|
||||
expect($this->output->capturedWrites)->not->toBeEmpty();
|
||||
});
|
||||
|
||||
it('works with DOTS style', function () {
|
||||
$spinner = new Spinner($this->output, 'Loading...', SpinnerStyle::DOTS, 0.01);
|
||||
$spinner->start();
|
||||
usleep(20000);
|
||||
$spinner->update();
|
||||
|
||||
expect($this->output->capturedWrites)->toBeArray();
|
||||
});
|
||||
|
||||
it('works with LINE style', function () {
|
||||
$spinner = new Spinner($this->output, 'Loading...', SpinnerStyle::LINE, 0.01);
|
||||
$spinner->start();
|
||||
usleep(20000);
|
||||
$spinner->update();
|
||||
|
||||
expect($this->output->capturedWrites)->toBeArray();
|
||||
});
|
||||
|
||||
it('works with BOUNCE style', function () {
|
||||
$spinner = new Spinner($this->output, 'Loading...', SpinnerStyle::BOUNCE, 0.01);
|
||||
$spinner->start();
|
||||
usleep(20000);
|
||||
$spinner->update();
|
||||
|
||||
expect($this->output->capturedWrites)->toBeArray();
|
||||
});
|
||||
|
||||
it('works with ARROW style', function () {
|
||||
$spinner = new Spinner($this->output, 'Loading...', SpinnerStyle::ARROW, 0.01);
|
||||
$spinner->start();
|
||||
usleep(20000);
|
||||
$spinner->update();
|
||||
|
||||
expect($this->output->capturedWrites)->toBeArray();
|
||||
});
|
||||
|
||||
it('cycles through frames correctly', function () {
|
||||
$spinner = new Spinner($this->output, 'Loading...', SpinnerStyle::LINE);
|
||||
$spinner->start();
|
||||
|
||||
// LINE style has 4 frames
|
||||
$frames = SpinnerStyle::LINE->getFrames();
|
||||
expect($frames)->toHaveCount(4);
|
||||
});
|
||||
|
||||
it('handles very fast interval', function () {
|
||||
$spinner = new Spinner($this->output, 'Loading...', SpinnerStyle::DOTS, 0.01);
|
||||
$spinner->start();
|
||||
usleep(50000); // 50ms
|
||||
$spinner->update();
|
||||
|
||||
expect($this->output->capturedWrites)->not->toBeEmpty();
|
||||
});
|
||||
|
||||
it('handles very slow interval', function () {
|
||||
$spinner = new Spinner($this->output, 'Loading...', SpinnerStyle::DOTS, 1.0);
|
||||
$spinner->start();
|
||||
$spinner->update();
|
||||
|
||||
// With slow interval, update may not write immediately
|
||||
expect($this->output->capturedWrites)->toBeArray();
|
||||
});
|
||||
});
|
||||
|
||||
describe('SpinnerStyle', function () {
|
||||
it('has all expected enum values', function () {
|
||||
expect(SpinnerStyle::DOTS->value)->toBe('dots');
|
||||
expect(SpinnerStyle::LINE->value)->toBe('line');
|
||||
expect(SpinnerStyle::BOUNCE->value)->toBe('bounce');
|
||||
expect(SpinnerStyle::ARROW->value)->toBe('arrow');
|
||||
});
|
||||
|
||||
it('provides frames for DOTS style', function () {
|
||||
$frames = SpinnerStyle::DOTS->getFrames();
|
||||
|
||||
expect($frames)->toBeArray();
|
||||
expect($frames)->not->toBeEmpty();
|
||||
expect($frames)->toContain('. ');
|
||||
expect($frames)->toContain('.. ');
|
||||
expect($frames)->toContain('...');
|
||||
});
|
||||
|
||||
it('provides frames for LINE style', function () {
|
||||
$frames = SpinnerStyle::LINE->getFrames();
|
||||
|
||||
expect($frames)->toBeArray();
|
||||
expect($frames)->toHaveCount(4);
|
||||
expect($frames)->toContain('|');
|
||||
expect($frames)->toContain('/');
|
||||
expect($frames)->toContain('-');
|
||||
expect($frames)->toContain('\\');
|
||||
});
|
||||
|
||||
it('provides frames for BOUNCE style', function () {
|
||||
$frames = SpinnerStyle::BOUNCE->getFrames();
|
||||
|
||||
expect($frames)->toBeArray();
|
||||
expect($frames)->not->toBeEmpty();
|
||||
});
|
||||
|
||||
it('provides frames for ARROW style', function () {
|
||||
$frames = SpinnerStyle::ARROW->getFrames();
|
||||
|
||||
expect($frames)->toBeArray();
|
||||
expect($frames)->toHaveCount(8);
|
||||
expect($frames)->toContain('←');
|
||||
expect($frames)->toContain('↑');
|
||||
expect($frames)->toContain('→');
|
||||
expect($frames)->toContain('↓');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user