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

244 lines
7.8 KiB
PHP

<?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('↓');
});
});