Skip to main content

Testing

Three harnesses ship, one per scope, and each runs the real thing at its own scope - so you assert on the answers and on what was drawn without a terminal anywhere in sight. The first two push keystrokes onto a scripted terminal's input pipe and run the session loop; the third hands them straight to one field, since a field on its own has no session to run.

HarnessDrivesReach for it when
TuiTestera whole form, through Tuiyou are testing a form the way a consumer runs it
ScreenTesterone screen, from a Panelyou are testing what was drawn, frame by frame
FieldRunnera single fieldyou are testing one field's keys in isolation

Keystrokes are Key objects and/or raw byte strings (the bytes a terminal emits for a key press), so an existing keystroke helper drops straight in.

A whole form

TuiTester wraps the Tui facade, so it runs exactly what a consumer's run() would.

use DrevOps\PhpTui\Input\Key;
use DrevOps\PhpTui\Input\KeyName;
use DrevOps\PhpTui\Testing\TuiTester;

$tester = new TuiTester($form);

$answers = $tester->run(
Key::named(KeyName::Enter), // go into the first panel
Key::named(KeyName::Enter), // open the "name" editor
'Ada', // type a value
Key::named(KeyName::Enter), // accept
Key::named(KeyName::Escape), // back to the root
Key::named(KeyName::Down), // move to Submit
Key::named(KeyName::Enter), // submit
);

// Raw bytes work too: $tester->run("\r", 'Ada', "\r", ...);

$this->assertSame('Ada', $answers->value('name'));
$this->assertStringContainsString('Ada', $tester->display());
$this->assertFalse($tester->isCancelled());

run() returns the collected Answers. display() is the ANSI-stripped output for substring assertions, output() the raw frames, and isCancelled() / isInterrupted() report how the run ended. theme(), layout(), options(), rows(), cols(), version(), directory() and update() tune the run before it starts.

The harness outside PHPUnit is shown in playground/13-testing.php.

One screen

ScreenTester is the screen-native companion. It takes the block tree directly - $form->root(), or a Panel you built yourself - and drives the same session loop, so it is the tool for asserting on what was drawn rather than on what came back.

use DrevOps\PhpTui\Input\Key;
use DrevOps\PhpTui\Input\KeyName;
use DrevOps\PhpTui\Testing\ScreenTester;

$tester = new ScreenTester($form->root());

$answers = $tester->cols(80)->rows(24)->run(
Key::named(KeyName::Enter),
'Ada',
Key::named(KeyName::Enter),
);

$this->assertSame('Ada', $answers->value('courier'));
$this->assertStringContainsString('Ada', $tester->frame());

Its display defaults are fixed rather than detected - no color, glyphs on, a dark palette, a terminal of a stated size - so a frame reads the same on every machine that runs the test.

What it adds over TuiTester is the frames. frames() hands back every frame in the order it was drawn, split on the screen clear that separates one from the next; frame($index) returns one of them ANSI-stripped, counting back from the last when the index is negative, so frame() is the frame the session ended on. output() and display() cover the whole stream as before.

Everything a session is built from can be set on it, which is what makes it the harness for a layout, a theme or a block you wrote yourself:

CallSets
theme(ThemeInterface)the theme the blocks draw through - any ThemeInterface, not a name
options(array)display options merged over the deterministic defaults
keys(KeyMap)the bindings the screen answers to
layout(string)the layout the screen is arranged by
border(Border)the frame drawn around every region at once
collector(Collector)what resolves the answers the form opens on
context(Context)the run the session belongs to
supplied(array)values supplied for the fields, keyed by field id
banner(string, string)what is shown before the form, and the version under it
footer(bool)whether the keys that apply right now are advertised
clearOnExit(bool)whether the screen is cleared as the session ends
externalEditor(...)what hands a passage of text to an editor of the reader's own
rows(int) / cols(int)the reported terminal size

theme() takes a ThemeInterface instance rather than a name, so a theme built for one test - an anonymous class overriding a single element, say - goes straight in with no registration, no construction contract to satisfy and no check that it can draw the whole form. That's the point: a test that drives one block through a theme answering for one element wants exactly that, and a block it can't draw says so by name when it's asked to. A theme that is a real class is better named on the facade, floor or not; see Themes.

One field

For a single field in isolation, FieldRunner::run($field, ArrayKeyStream::of(...)) stays the lighter tool: no screen, no session, just the field and the keys you hand it.