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