Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 145 additions & 0 deletions tests/Unit/EventBusTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
<?php

declare(strict_types=1);

use PhpTui\Term\Event\CharKeyEvent;
use PhpTui\Term\Event\CodedKeyEvent;
use PhpTui\Term\Event\FunctionKeyEvent;
use PhpTui\Term\Event\MouseEvent;
use PhpTui\Term\KeyCode;
use PhpTui\Term\KeyModifiers;
use PhpTui\Term\MouseButton;
use PhpTui\Term\MouseEventKind;
use Tapper\Console\EventBus;

describe('string/int events', function () {
it('dispatches a registered string event to its listener', function () {
$bus = new EventBus;
$received = null;
$bus->listen('custom', function ($data) use (&$received) {
$received = $data;
});

$bus->emit('custom', ['foo' => 'bar']);

expect($received)->toBe(['foo' => 'bar']);
});

it('does nothing when emitting an event with no listeners', function () {
$bus = new EventBus;

$bus->emit('nobody-listens');

expect(true)->toBeTrue();
});

it('calls every listener registered for the same event, in order', function () {
$bus = new EventBus;
$calls = [];
$bus->listen('tick', function () use (&$calls) {
$calls[] = 'first';
});
$bus->listen('tick', function () use (&$calls) {
$calls[] = 'second';
});

$bus->emit('tick');

expect($calls)->toBe(['first', 'second']);
});
});

describe('KeyCode/MouseEventKind registration', function () {
it('registers a KeyCode listener under its enum name', function () {
$bus = new EventBus;
$fired = false;
$bus->listen(KeyCode::Enter, function () use (&$fired) {
$fired = true;
});

$bus->emit('Enter');

expect($fired)->toBeTrue();
});

it('registers a MouseEventKind listener under a "Mouse"-prefixed name', function () {
$bus = new EventBus;
$fired = false;
$bus->listen(MouseEventKind::Down, function () use (&$fired) {
$fired = true;
});

$bus->emit('MouseDown');

expect($fired)->toBeTrue();
});
});

describe('CharKeyEvent dispatch', function () {
it('dispatches keyed by the typed character and merges modifiers into the data', function () {
$bus = new EventBus;
$received = null;
$bus->listen('a', function ($data) use (&$received) {
$received = $data;
});

$bus->emit(CharKeyEvent::new('a', KeyModifiers::SHIFT));

expect($received)->toBe(['modifiers' => KeyModifiers::SHIFT]);
});

it('lets extra data passed to emit override the default modifiers key', function () {
$bus = new EventBus;
$received = null;
$bus->listen('b', function ($data) use (&$received) {
$received = $data;
});

$bus->emit(CharKeyEvent::new('b'), ['modifiers' => 'overridden']);

expect($received)->toBe(['modifiers' => 'overridden']);
});
});

describe('CodedKeyEvent dispatch', function () {
it('dispatches keyed by the KeyCode enum name', function () {
$bus = new EventBus;
$fired = false;
$bus->listen(KeyCode::Esc->name, function () use (&$fired) {
$fired = true;
});

$bus->emit(CodedKeyEvent::new(KeyCode::Esc));

expect($fired)->toBeTrue();
});
});

describe('FunctionKeyEvent dispatch', function () {
it('dispatches keyed by "F{number}"', function () {
$bus = new EventBus;
$fired = false;
$bus->listen('F5', function () use (&$fired) {
$fired = true;
});

$bus->emit(FunctionKeyEvent::new(5));

expect($fired)->toBeTrue();
});
});

describe('MouseEvent dispatch', function () {
it('dispatches keyed by "Mouse{kind}" and passes the event back in the data', function () {
$bus = new EventBus;
$received = null;
$bus->listen('MouseScrollUp', function ($data) use (&$received) {
$received = $data;
});

$event = MouseEvent::new(MouseEventKind::ScrollUp, MouseButton::None, column: 3, row: 4, modifiers: 0);
$bus->emit($event);

expect($received)->toBe(['event' => $event]);
});
});
124 changes: 124 additions & 0 deletions tests/Unit/MessageFormatterTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
<?php

declare(strict_types=1);

use PhpTui\Tui\Text\Line;
use PhpTui\Tui\Text\Span;
use Tapper\Console\MessageFormatter;
use Tapper\Console\Palette;

function inlineJoined(array $spans): string
{
return implode('', array_map(fn (Span $s): string => $s->content, $spans));
}

function formattedJoined(array $lines): string
{
return implode("\n", array_map(
fn (Line $line): string => implode('', array_map(fn (Span $s): string => $s->content, $line->spans)),
$lines,
));
}

describe('colorizeInlineJson', function () {
it('renders a JSON string as quoted spans', function () {
$spans = MessageFormatter::colorizeInlineJson('"hello"');

expect(inlineJoined($spans))->toBe('"hello"');
});

it('renders a JSON number', function () {
$spans = MessageFormatter::colorizeInlineJson('42');

expect(inlineJoined($spans))->toBe('42');
});

it('renders JSON booleans as bare true/false', function () {
expect(inlineJoined(MessageFormatter::colorizeInlineJson('true')))->toBe('true')
->and(inlineJoined(MessageFormatter::colorizeInlineJson('false')))->toBe('false');
});

it('renders JSON null as the bare word null', function () {
$spans = MessageFormatter::colorizeInlineJson('null');

expect(inlineJoined($spans))->toBe('null');
});

it('renders a JSON list with brackets and comma separators', function () {
$spans = MessageFormatter::colorizeInlineJson('[1,2,3]');

expect(inlineJoined($spans))->toBe('[1, 2, 3]');
});

it('renders a JSON object with braces and quoted keys', function () {
$spans = MessageFormatter::colorizeInlineJson('{"a":1,"b":2}');

expect(inlineJoined($spans))->toBe('{"a": 1, "b": 2}');
});

it('renders nested structures recursively', function () {
$spans = MessageFormatter::colorizeInlineJson('{"list":[1,{"x":"y"}]}');

expect(inlineJoined($spans))->toBe('{"list": [1, {"x": "y"}]}');
});

it('falls back to a single error-styled span for invalid JSON', function () {
$spans = MessageFormatter::colorizeInlineJson('{not valid json');

expect($spans)->toHaveCount(1)
->and($spans[0]->content)->toBe('{not valid json')
->and($spans[0]->style->fg->toHex())->toBe('#'.Palette::ERROR);
});
});

describe('colorizeFormattedJson', function () {
it('renders a scalar as a single line', function () {
$lines = MessageFormatter::colorizeFormattedJson('"hi"');

expect($lines)->toHaveCount(1)
->and(formattedJoined($lines))->toBe('"hi"');
});

it('renders an object across multiple indented lines with closing brace', function () {
$lines = MessageFormatter::colorizeFormattedJson('{"a":1,"b":2}');

expect(formattedJoined($lines))->toBe(
"{\n"
.' "a": 1'."\n"
.' "b": 2'."\n"
.'}'
);
});

it('renders a list using square brackets without keys', function () {
$lines = MessageFormatter::colorizeFormattedJson('[1,2]');

expect(formattedJoined($lines))->toBe(
"[\n"
.' 1'."\n"
.' 2'."\n"
.']'
);
});

it('indents nested objects one level deeper than their parent', function () {
$lines = MessageFormatter::colorizeFormattedJson('{"a":{"b":1}}');

expect(formattedJoined($lines))->toBe(
"{\n"
.' "a": '."\n"
.' {'."\n"
.' "b": 1'."\n"
.' }'."\n"
.'}'
);
});

it('falls back to a single error-styled line for invalid JSON', function () {
$lines = MessageFormatter::colorizeFormattedJson('{broken');

expect($lines)->toHaveCount(1)
->and(formattedJoined($lines))->toBe('{broken')
->and($lines[0]->spans[0]->style->fg->toHex())->toBe('#'.Palette::ERROR);
});
});
98 changes: 98 additions & 0 deletions tests/Unit/PhpHighlighterTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?php

declare(strict_types=1);

use Tapper\Console\PhpHighlighter;

function spanFor(array $spans, string $content): ?PhpTui\Tui\Text\Span
{
foreach ($spans as $span) {
if ($span->content === $content) {
return $span;
}
}

return null;
}

describe('highlightLine', function () {
it('reconstructs the original line when spans are joined back together', function () {
$line = 'function foo($bar) { return $bar; }';

$spans = PhpHighlighter::highlightLine($line);

expect(implode('', array_map(fn ($s) => $s->content, $spans)))->toBe($line);
});

it('colors a language keyword', function () {
$spans = PhpHighlighter::highlightLine('return $x;');

$span = spanFor($spans, 'return');

expect($span)->not->toBeNull()
->and($span->style->fg->toHex())->toBe('#bb9af7');
});

it('colors a variable', function () {
$spans = PhpHighlighter::highlightLine('$foo = 1;');

$span = spanFor($spans, '$foo');

expect($span)->not->toBeNull()
->and($span->style->fg->toHex())->toBe('#7dcfff');
});

it('colors a string literal', function () {
$spans = PhpHighlighter::highlightLine("\$x = 'hello';");

$span = spanFor($spans, "'hello'");

expect($span)->not->toBeNull()
->and($span->style->fg->toHex())->toBe('#9ece6a');
});

it('colors an integer literal', function () {
$spans = PhpHighlighter::highlightLine('$x = 42;');

$span = spanFor($spans, '42');

expect($span)->not->toBeNull()
->and($span->style->fg->toHex())->toBe('#ff9e64');
});

it('colors a comment', function () {
$spans = PhpHighlighter::highlightLine('$x = 1; // note');

$span = spanFor($spans, '// note');

expect($span)->not->toBeNull()
->and($span->style->fg->toHex())->toBe('#565f89');
});

it('colors bare keywords like true/false/null as keywords, not plain identifiers', function () {
$spans = PhpHighlighter::highlightLine('$x = true;');

$span = spanFor($spans, 'true');

expect($span)->not->toBeNull()
->and($span->style->fg->toHex())->toBe('#bb9af7');
});

it('colors a function call name differently from a bare identifier', function () {
$spans = PhpHighlighter::highlightLine('strlen($x);');

$span = spanFor($spans, 'strlen');

expect($span)->not->toBeNull()
->and($span->style->fg->toHex())->toBe('#7aa2f7');
});

it('falls back to the default text color for a plain identifier that is not a call', function () {
$spans = PhpHighlighter::highlightLine('$x = SOME_CONST;');

$span = spanFor($spans, 'SOME_CONST');

expect($span)->not->toBeNull()
->and($span->style->fg->toHex())->toBe('#c0caf5');
});
});
Loading
Loading