diff options
Diffstat (limited to 'ttwhy/routers/echo.tests.cpp')
| -rw-r--r-- | ttwhy/routers/echo.tests.cpp | 235 |
1 files changed, 235 insertions, 0 deletions
diff --git a/ttwhy/routers/echo.tests.cpp b/ttwhy/routers/echo.tests.cpp new file mode 100644 index 0000000..f12d08b --- /dev/null +++ b/ttwhy/routers/echo.tests.cpp @@ -0,0 +1,235 @@ +#include <asio.hpp> + +#include <catch2/catch_test_macros.hpp> + +#include <string> +#include <string_view> +#include <utility> + +import ttwhy.routers; +import ttwhy.scanners; + +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); + } + }; +} // namespace + +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::routers::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::scanners::character_event{'A'})); + run_coro(router.process(ttwhy::scanners::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::scanners::navigation_event{ttwhy::scanners::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::scanners::navigation_event{ttwhy::scanners::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::scanners::navigation_event{ttwhy::scanners::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::scanners::navigation_event{ttwhy::scanners::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::scanners::navigation_event{ttwhy::scanners::navigation_key::left})); + stream.written.clear(); + run_coro(router.process(ttwhy::scanners::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::scanners::navigation_event{ttwhy::scanners::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::scanners::navigation_event{ttwhy::scanners::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::scanners::navigation_event{ttwhy::scanners::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::scanners::control_event{ttwhy::scanners::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::scanners::control_event{ttwhy::scanners::control_key::enter})); + stream.written.clear(); + run_coro(router.process(ttwhy::scanners::control_event{ttwhy::scanners::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::scanners::navigation_event{ttwhy::scanners::navigation_key::left})); + stream.written.clear(); + run_coro(router.process(ttwhy::scanners::navigation_event{ttwhy::scanners::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::scanners::navigation_event{ttwhy::scanners::navigation_key::delete_key})); + + THEN("Nothing is emitted") + { + CHECK(stream.written.empty()); + CHECK(router.cursor_position() == 1); + CHECK(router.max_position() == 1); + } + } + } + } + } +} |
