Modules/Graphics/tests/InputRecordingTests.cpp
Шурупов Илья Викторович bf94dd0f5f Split window input types into a GPU-free GraphicsInput library
Input codes, pointer samples, the coalescer, key mapping and input
recordings have no window or Vulkan dependency. As GraphicsInput they can
be linked by headless libraries, and Widgets now links it instead of
reaching into Graphics/public by include path. InputRecorder can start its
clock explicitly so a recording measures from when it was started, not
from its first event.
2026-10-15 20:54:04 +03:00

126 lines
4.1 KiB
C++

#include "InputCoalescer.hpp"
#include "InputRecording.hpp"
#include "UnitTest++/UnitTest++.h"
#include <vector>
using namespace tp;
namespace {
struct ManualClock {
double now = 0;
InputRecorder::Clock clock() {
return [this]() { return now; };
}
};
struct Collector : WindowInputObserver {
std::vector<std::string> calls;
void onPointerSample(const PointerSample& sample) override {
calls.push_back("sample " + std::to_string(sample.pressure));
}
void onPointerMoved(Vec2F position) override {
calls.push_back("move " + std::to_string((int) position.x) + " " + std::to_string((int) position.y));
}
void onPointerButton(InputID, bool pressed) override { calls.push_back(pressed ? "down" : "up"); }
void onKey(InputID, bool pressed) override { calls.push_back(pressed ? "key down" : "key up"); }
};
InputRecording penStroke() {
ManualClock time;
InputRecorder recorder(time.clock());
recorder.setWindowSize({ 1600, 1000 });
PointerSample pen;
pen.tool = PointerTool::Pen;
pen.pressure = 0.25f;
pen.tilt = { 0.1f, -0.2f };
pen.timeMs = 77;
time.now = 1000;
recorder.onKey(InputID::LEFT_SHIFT, true);
time.now = 1010;
recorder.onPointerSample(pen);
recorder.onPointerMoved({ 10, 20 });
recorder.onPointerButton(InputID::MOUSE1, true);
time.now = 1012.5;
recorder.onPointerMoved({ 30, 40 });
time.now = 1030;
recorder.onPointerButton(InputID::MOUSE1, false);
time.now = 1200;
recorder.onKey(InputID::LEFT_SHIFT, false);
return recorder.recording();
}
}
SUITE(InputRecording) {
TEST(RecorderTimesEventsFromTheFirstOne) {
const InputRecording recording = penStroke();
REQUIRE CHECK_EQUAL(7u, recording.events.size());
CHECK_EQUAL(0.0, recording.events[0].timeMs);
CHECK_EQUAL(12.5, recording.events[4].timeMs);
CHECK_EQUAL(200.0, recording.durationMs());
}
TEST(ExplicitClockStartTimesTheFirstEventFromThere) {
ManualClock time;
InputRecorder recorder(time.clock());
time.now = 500;
recorder.startClockNow();
time.now = 740;
recorder.onPointerMoved({ 1, 1 });
CHECK_EQUAL(240.0, recorder.recording().events.front().timeMs);
}
TEST(SerializedRecordingParsesBackIdentically) {
const InputRecording recording = penStroke();
const std::optional<InputRecording> parsed = InputRecording::parse(recording.serialize());
REQUIRE CHECK(parsed.has_value());
CHECK_EQUAL(1600.0f, parsed->windowSize.x);
REQUIRE CHECK_EQUAL(recording.events.size(), parsed->events.size());
for (size_t index = 0; index < recording.events.size(); index++)
CHECK(recording.events[index] == parsed->events[index]);
}
TEST(ParseRejectsForeignText) {
CHECK(!InputRecording::parse("hello\n").has_value());
CHECK(!InputRecording::parse(std::string(InputRecording::kHeader) + "\n1.0 teleport 3\n").has_value());
}
TEST(ReplayDeliversEventsInOrder) {
Collector collector;
for (const RecordedInput& event : penStroke().events)
event.deliverTo(collector);
const std::vector<std::string> expected = { "key down", "sample 0.250000", "move 10 20", "down", "move 30 40",
"up", "key up" };
CHECK(expected == collector.calls);
}
TEST(SliceKeepsThePointerActivityAndRebasesTime) {
const InputRecording sliced = penStroke().slicedToPointerActivity();
REQUIRE CHECK_EQUAL(5u, sliced.events.size());
CHECK(sliced.events.front().kind == RecordedInput::Kind::Sample);
CHECK_EQUAL(0.0, sliced.events.front().timeMs);
CHECK(sliced.events.back().kind == RecordedInput::Kind::Button);
CHECK_EQUAL(20.0, sliced.events.back().timeMs);
}
TEST(TranslateMovesOnlyPointerPositions) {
const InputRecording moved = penStroke().translated({ 5, -5 });
CHECK_EQUAL(15.0f, moved.events[2].vector.x);
CHECK_EQUAL(15.0f, moved.events[2].vector.y);
CHECK_EQUAL(0.25f, moved.events[1].sample.pressure);
}
TEST(CoalescerTapSeesEveryRawEventBeforeCoalescing) {
InputCoalescer coalescer;
Collector tap;
coalescer.setTap(&tap);
coalescer.recorder().onPointerMoved({ 1, 1 });
coalescer.recorder().onPointerMoved({ 2, 2 });
coalescer.recorder().onPointerMoved({ 3, 3 });
CHECK_EQUAL(3u, tap.calls.size());
CHECK_EQUAL(1u, coalescer.pendingEvents());
}
}