diff options
| author | Felix Morgner <felix.morgner@gmail.com> | 2026-09-22 22:47:33 +0200 |
|---|---|---|
| committer | Felix Morgner <felix.morgner@gmail.com> | 2026-09-22 22:47:33 +0200 |
| commit | d05b9343f69a0841043963088e0003368e205bd9 (patch) | |
| tree | 8cde07ff86d27cbc808a0a149b89d0024b4a586c /ttwhy/terminal/echo.tests.cpp | |
| parent | 22f6534081acfabdab4956d337604a21e7da5b64 (diff) | |
| download | ttwhy-master.tar.xz ttwhy-master.zip | |
Diffstat (limited to 'ttwhy/terminal/echo.tests.cpp')
| -rw-r--r-- | ttwhy/terminal/echo.tests.cpp | 257 |
1 files changed, 257 insertions, 0 deletions
diff --git a/ttwhy/terminal/echo.tests.cpp b/ttwhy/terminal/echo.tests.cpp new file mode 100644 index 0000000..ba07d55 --- /dev/null +++ b/ttwhy/terminal/echo.tests.cpp @@ -0,0 +1,257 @@ +#include <asio.hpp> + +#include <catch2/catch_test_macros.hpp> + +#include <string> +#include <string_view> +#include <utility> + +import ttwhy.core; +import ttwhy.terminal; + +using namespace std::string_view_literals; + +namespace +{ + struct mock_stream + { + using executor_type = asio::any_io_executor; + + asio::any_io_executor executor; + std::string written{}; + + explicit mock_stream(asio::any_io_executor exec) + : executor{std::move(exec)} + {} + + auto get_executor() const noexcept -> executor_type + { + return executor; + } + + template<typename ConstBufferSequence, typename Token> + auto async_write_some(ConstBufferSequence const & buffers, Token && token) + { + return asio::async_initiate<Token, void(asio::error_code, std::size_t)>( + [this](auto handler, ConstBufferSequence bufs) { + auto const total = asio::buffer_size(bufs); + auto const offset = written.size(); + written.resize(offset + total); + asio::buffer_copy(asio::buffer(written.data() + offset, total), bufs); + asio::post(executor, [handler = std::move(handler), total]() mutable { + std::move(handler)(asio::error_code{}, total); + }); + }, + token, buffers); + } + }; + + struct incomplete_router_no_process + { + }; + + struct incomplete_router_wrong_return + { + auto process(ttwhy::terminal::input_event) -> void; + }; + + struct incomplete_router_wrong_event + { + auto process(int) -> asio::awaitable<void>; + }; +} // namespace + +TEST_CASE("The router concept validates router candidate interfaces", "[router][concept]") +{ + STATIC_REQUIRE(ttwhy::router<ttwhy::terminal::echo<mock_stream>, ttwhy::terminal::input_event>); + STATIC_REQUIRE_FALSE(ttwhy::router<incomplete_router_no_process, ttwhy::terminal::input_event>); + STATIC_REQUIRE_FALSE(ttwhy::router<incomplete_router_wrong_return, ttwhy::terminal::input_event>); + STATIC_REQUIRE_FALSE(ttwhy::router<incomplete_router_wrong_event, ttwhy::terminal::input_event>); +} + +SCENARIO("The echo router handles terminal navigation within the valid printed area", "[router][echo]") +{ + GIVEN("An initialized echo router with a mock stream") + { + auto context = asio::io_context{}; + auto stream = mock_stream{context.get_executor()}; + auto router = ttwhy::terminal::echo{stream}; + + auto run_coro = [&](asio::awaitable<void> coro) { + asio::co_spawn(context, std::move(coro), asio::detached); + context.restart(); + context.run(); + }; + + WHEN("Typing characters") + { + run_coro(router.process(ttwhy::terminal::character_event{'A'})); + run_coro(router.process(ttwhy::terminal::character_event{'B'})); + + THEN("Characters are echoed and positions advance") + { + CHECK(stream.written == "AB"); + CHECK(router.cursor_position() == 2); + CHECK(router.max_position() == 2); + } + + AND_WHEN("Navigating left") + { + stream.written.clear(); + run_coro(router.process(ttwhy::terminal::navigation_event{ttwhy::terminal::navigation_key::left})); + + THEN("The cursor moves left") + { + CHECK(stream.written == "\x1b[D"); + CHECK(router.cursor_position() == 1); + CHECK(router.max_position() == 2); + } + + AND_WHEN("Navigating left again to position 0") + { + stream.written.clear(); + run_coro(router.process(ttwhy::terminal::navigation_event{ttwhy::terminal::navigation_key::left})); + + THEN("The cursor reaches 0") + { + CHECK(stream.written == "\x1b[D"); + CHECK(router.cursor_position() == 0); + CHECK(router.max_position() == 2); + } + + AND_WHEN("Attempting to navigate left beyond 0") + { + stream.written.clear(); + run_coro(router.process(ttwhy::terminal::navigation_event{ttwhy::terminal::navigation_key::left})); + + THEN("Navigation is prevented and nothing is emitted") + { + CHECK(stream.written.empty()); + CHECK(router.cursor_position() == 0); + CHECK(router.max_position() == 2); + } + } + } + } + + AND_WHEN("Attempting to navigate right beyond what was printed") + { + stream.written.clear(); + run_coro(router.process(ttwhy::terminal::navigation_event{ttwhy::terminal::navigation_key::right})); + + THEN("Navigation is prevented and nothing is emitted") + { + CHECK(stream.written.empty()); + CHECK(router.cursor_position() == 2); + CHECK(router.max_position() == 2); + } + } + + AND_WHEN("Navigating left and then overwriting with new character") + { + run_coro(router.process(ttwhy::terminal::navigation_event{ttwhy::terminal::navigation_key::left})); + stream.written.clear(); + run_coro(router.process(ttwhy::terminal::character_event{'X'})); + + THEN("The new character is written and max position is preserved") + { + CHECK(stream.written == "X"); + CHECK(router.cursor_position() == 2); + CHECK(router.max_position() == 2); + } + } + + AND_WHEN("Using Home and End keys") + { + stream.written.clear(); + run_coro(router.process(ttwhy::terminal::navigation_event{ttwhy::terminal::navigation_key::home})); + + THEN("Home navigates to 0") + { + CHECK(stream.written == "\x1b[2D"); + CHECK(router.cursor_position() == 0); + CHECK(router.max_position() == 2); + } + + AND_WHEN("Pressing Home when already at 0") + { + stream.written.clear(); + run_coro(router.process(ttwhy::terminal::navigation_event{ttwhy::terminal::navigation_key::home})); + + THEN("Nothing is emitted") + { + CHECK(stream.written.empty()); + CHECK(router.cursor_position() == 0); + } + } + + AND_WHEN("Pressing End from 0") + { + stream.written.clear(); + run_coro(router.process(ttwhy::terminal::navigation_event{ttwhy::terminal::navigation_key::end})); + + THEN("End moves to max position") + { + CHECK(stream.written == "\x1b[2C"); + CHECK(router.cursor_position() == 2); + CHECK(router.max_position() == 2); + } + } + } + + AND_WHEN("Pressing Enter") + { + stream.written.clear(); + run_coro(router.process(ttwhy::terminal::control_event{ttwhy::terminal::control_key::enter})); + + THEN("Newline is emitted and positions reset") + { + CHECK(stream.written == "\r\n"); + CHECK(router.cursor_position() == 0); + CHECK(router.max_position() == 0); + } + } + + AND_WHEN("Pressing Backspace at position 0 after reset") + { + run_coro(router.process(ttwhy::terminal::control_event{ttwhy::terminal::control_key::enter})); + stream.written.clear(); + run_coro(router.process(ttwhy::terminal::control_event{ttwhy::terminal::control_key::backspace})); + + THEN("Backspace does nothing at position 0") + { + CHECK(stream.written.empty()); + CHECK(router.cursor_position() == 0); + CHECK(router.max_position() == 0); + } + } + + AND_WHEN("Using Delete key") + { + run_coro(router.process(ttwhy::terminal::navigation_event{ttwhy::terminal::navigation_key::left})); + stream.written.clear(); + run_coro(router.process(ttwhy::terminal::navigation_event{ttwhy::terminal::navigation_key::delete_key})); + + THEN("Delete character sequence is emitted and max position shrinks") + { + CHECK(stream.written == "\x1b[P"); + CHECK(router.cursor_position() == 1); + CHECK(router.max_position() == 1); + } + + AND_WHEN("Pressing Delete when cursor is at max position") + { + stream.written.clear(); + run_coro(router.process(ttwhy::terminal::navigation_event{ttwhy::terminal::navigation_key::delete_key})); + + THEN("Nothing is emitted") + { + CHECK(stream.written.empty()); + CHECK(router.cursor_position() == 1); + CHECK(router.max_position() == 1); + } + } + } + } + } +} |
