- 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
244 lines
7.8 KiB
PHP
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('↓');
|
|
});
|
|
});
|
|
|