aboutsummaryrefslogtreecommitdiff
path: root/ttwhy/terminal/echo.tests.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'ttwhy/terminal/echo.tests.cpp')
-rw-r--r--ttwhy/terminal/echo.tests.cpp257
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);
+ }
+ }
+ }
+ }
+ }
+}