#include #include #include #include #include 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 auto async_write_some(ConstBufferSequence const & buffers, Token && token) { return asio::async_initiate( [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_policy { auto process(ttwhy::scanners::input_event) -> asio::awaitable; }; struct incomplete_router_no_process { using terminal_policy = ttwhy::scanners::xterm_policy; }; struct incomplete_router_wrong_return { using terminal_policy = ttwhy::scanners::xterm_policy; auto process(ttwhy::scanners::input_event) -> void; }; } // namespace TEST_CASE("The router concept validates router candidate interfaces", "[router][concept]") { STATIC_REQUIRE(ttwhy::router>); STATIC_REQUIRE_FALSE(ttwhy::router); STATIC_REQUIRE_FALSE(ttwhy::router); STATIC_REQUIRE_FALSE(ttwhy::router); } 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 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); } } } } } }