Test Components
Drive a component against a fake terminal with TestScreen — send it input and assert on what it renders.
Overview
TestScreen renders into an in-memory buffer instead of a real terminal, so tests run under Pest or PHPUnit like any other — no TTY, no subprocess. You mount a component at a chosen cell size, send it keys, clicks and elapsed time, then assert on the rendered frame, the emitted events, or a returned value.
Mount a component
TestScreen::mount() takes the root component and a cell size. A component with its own constructor is mounted with new; anything a parent would supply needs a small host.
Mount and assert
Mount at a chosen size, then assert on the rendered frame:
use Phui\Testing\TestScreen;
it('greets the user', function () {
TestScreen::mount(new Greeter('Sam'), 40, 10)
->assertSee('Hello, Sam');
});assertSee() and assertDontSee() match against the plain text of the frame. For anything finer, take a Capture with capture() and read it as text or cell by cell.
With props and models
Props and models are supplied by a parent, and emitted events are handled by one — so to test those, give the component a small host. An anonymous component is enough:
$host = new class extends Component {
public function build(): Node
{
return Select::mount()->props(
placeholder: 'Pick a fruit',
items: ['Apple', 'Banana', 'Cherry'],
);
}
};
TestScreen::mount($host, 40, 10)->assertSee('Pick a fruit');Drive input
Every input method returns the screen, so calls chain. Drive the component the way a user would, asserting between steps.
Send keys and clicks
use Phui\Events\Keyboard\Keypress;
$screen = TestScreen::mount(new Counter, 20, 3);
$screen->press(Keypress::ARROW_UP)->assertSee('1');
$screen->type('hello');
$screen->click(4, 2);press() sends one keypress, type() sends a string a character at a time, and click() and scroll() send mouse events at a cell.
Advance time
Timers and animations do not fire on their own in a test — move the fake clock forward with advance(). A component that increments on a one-second interval:
$screen = TestScreen::mount(new Ticker, 30, 3);
$screen->assertSee('Ticks: 0');
$screen->advance(1000);
$screen->assertSee('Ticks: 1');Wait for async work
When a handler awaits, the work resolves between frames. settle() ticks the screen until nothing is pending, so the resolved state is on screen before you assert:
$screen = TestScreen::mount(new UserList, 40, 20);
$screen->press(Keypress::r)->settle();
$screen->assertSee('Ada Lovelace');If the work resolves on an external event loop, pass a closure to settle() to drive that loop between ticks.
Assert on output
Beyond assertSee(), catch what a component emits, snapshot its frame, or check the value it completes with.
Emitted events
Catch what the component emits from its host, reading the payload off the handler's EmitEvent:
use Phui\Events\EmitEvent;
$picked = new stdClass;
$host = new class($picked) extends Component {
public function __construct(private stdClass $picked) {}
public function build(): Node
{
return Select::mount()
->props(items: ['Apple', 'Banana'])
->on('change', fn (EmitEvent $e) => $this->picked->value = $e->data);
}
};
$screen = TestScreen::mount($host, 40, 10);
$screen->press(Keypress::ENTER)
->press(Keypress::ARROW_DOWN)
->press(Keypress::ENTER);
expect($picked->value)->toBe('Banana');Snapshots
Phui produces the frame; your test framework stores and compares it. There is no snapshot storage in Phui itself — hand a Capture to your framework's snapshot assertion:
it('renders the dashboard', function () {
$screen = TestScreen::mount(new Dashboard, 60, 16)->press(Keypress::j);
expect($screen->capture()->toAnsi())->toMatchSnapshot();
});Snapshot toText() when only the layout matters and styling churn should not fail the test; snapshot toAnsi() when colours and decorations are part of the contract. Either way the comparison is over the terminal cell grid, not rendered pixels, so snapshots are deterministic across machines and CI.
Completion
A component that ends the run with complete() exposes the result:
$screen = TestScreen::mount(new Wizard, 40, 20);
$screen->press(Keypress::ENTER);
expect($screen->completed())->toBeTrue();
expect($screen->result())->toBe('confirmed');