0) { ob_end_clean(); } }); afterEach(function () { // Cleanup after tests while (ob_get_level() > 0) { ob_end_clean(); } }); describe('capture()', function () { it('captures output from callback', function () { $captured = OutputBuffer::capture(function () { echo "Hello World"; return 42; }); expect($captured)->toBeInstanceOf(CapturedOutput::class); expect($captured->content)->toBe("Hello World"); expect($captured->result)->toBe(42); }); it('captures empty output', function () { $captured = OutputBuffer::capture(function () { return "result"; }); expect($captured->content)->toBe(""); expect($captured->result)->toBe("result"); expect($captured->isEmpty())->toBeTrue(); }); it('captures multiline output', function () { $captured = OutputBuffer::capture(function () { echo "Line 1\n"; echo "Line 2\n"; echo "Line 3"; }); expect($captured->content)->toBe("Line 1\nLine 2\nLine 3"); expect($captured->lineCount())->toBe(3); expect($captured->lines())->toBe(['Line 1', 'Line 2', 'Line 3']); }); it('cleans up buffer on success', function () { $levelBefore = ob_get_level(); OutputBuffer::capture(function () { echo "test"; }); expect(ob_get_level())->toBe($levelBefore); }); it('cleans up buffer on exception', function () { $levelBefore = ob_get_level(); try { OutputBuffer::capture(function () { echo "test"; throw new RuntimeException('Test exception'); }); } catch (RuntimeException $e) { // Expected exception } expect(ob_get_level())->toBe($levelBefore); }); it('supports custom configuration', function () { $callbackExecuted = false; $config = OutputBufferConfig::withCallback(function (string $buffer) use (&$callbackExecuted) { $callbackExecuted = true; return strtoupper($buffer); }); $captured = OutputBuffer::capture(function () { echo "hello"; }, $config); expect($callbackExecuted)->toBeTrue(); expect($captured->content)->toBe("HELLO"); }); it('handles nested captures', function () { $captured1 = OutputBuffer::capture(function () { echo "Outer Start\n"; $captured2 = OutputBuffer::capture(function () { echo "Inner"; return "inner-result"; }); echo $captured2->content . " processed\n"; echo "Outer End"; return "outer-result"; }); expect($captured1->content)->toBe("Outer Start\nInner processed\nOuter End"); expect($captured1->result)->toBe("outer-result"); }); }); describe('start()', function () { it('starts new buffer and returns handle', function () { $levelBefore = ob_get_level(); $handle = OutputBuffer::start(); expect(ob_get_level())->toBe($levelBefore + 1); expect($handle->level)->toBe($levelBefore + 1); expect($handle->isActive())->toBeTrue(); $handle->end(); }); it('allows manual buffer management', function () { $handle = OutputBuffer::start(); echo "Test output"; $content = $handle->clean(); expect($content)->toBe("Test output"); expect(ob_get_level())->toBe(0); }); it('supports custom configuration', function () { $config = OutputBufferConfig::forCapture(); $handle = OutputBuffer::start($config); expect($handle->config)->toBe($config); $handle->end(); }); }); describe('getCurrentLevel()', function () { it('returns current buffer level', function () { expect(OutputBuffer::getCurrentLevel())->toBe(0); ob_start(); expect(OutputBuffer::getCurrentLevel())->toBe(1); ob_start(); expect(OutputBuffer::getCurrentLevel())->toBe(2); ob_end_clean(); ob_end_clean(); }); }); describe('isActive()', function () { it('returns false when no buffers active', function () { expect(OutputBuffer::isActive())->toBeFalse(); }); it('returns true when buffer is active', function () { ob_start(); expect(OutputBuffer::isActive())->toBeTrue(); ob_end_clean(); }); }); describe('cleanAll()', function () { it('cleans all active buffers', function () { ob_start(); ob_start(); ob_start(); expect(ob_get_level())->toBe(3); $count = OutputBuffer::cleanAll(); expect($count)->toBe(3); expect(ob_get_level())->toBe(0); }); it('returns 0 when no buffers active', function () { $count = OutputBuffer::cleanAll(); expect($count)->toBe(0); }); }); describe('getStatus()', function () { it('returns status of all buffers', function () { $status = OutputBuffer::getStatus(); expect($status)->toBeArray(); }); it('shows buffer information when active', function () { ob_start(); $status = OutputBuffer::getStatus(); expect($status)->toBeArray(); expect($status)->toHaveCount(1); ob_end_clean(); }); }); describe('withoutBuffering()', function () { it('executes callback without buffering', function () { $executedWithoutBuffer = false; OutputBuffer::capture(function () use (&$executedWithoutBuffer) { OutputBuffer::withoutBuffering(function () use (&$executedWithoutBuffer) { $executedWithoutBuffer = ob_get_level() === 0; }); }); expect($executedWithoutBuffer)->toBeTrue(); }); it('restores buffers after callback', function () { $captured = OutputBuffer::capture(function () { echo "Before"; OutputBuffer::withoutBuffering(function () { // This output should not be captured echo "During"; }); echo "After"; }); expect($captured->content)->toBe("BeforeAfter"); }); it('returns callback result', function () { $result = OutputBuffer::withoutBuffering(function () { return 42; }); expect($result)->toBe(42); }); }); describe('exception handling', function () { it('throws exception when capture fails', function () { // This is hard to test as ob_start rarely fails // but we test the exception exists expect(OutputBufferException::failedToCapture()) ->toBeInstanceOf(OutputBufferException::class); }); it('propagates callback exceptions', function () { expect(function () { OutputBuffer::capture(function () { throw new RuntimeException('Test'); }); })->toThrow(RuntimeException::class); }); }); });