Skip to content

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:

php
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:

php
$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 ​

php
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:

php
$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:

php
$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:

php
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:

php
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:

php
$screen = TestScreen::mount(new Wizard, 40, 20);

$screen->press(Keypress::ENTER);

expect($screen->completed())->toBeTrue();
expect($screen->result())->toBe('confirmed');

Released under the MIT License.