mirror of
https://github.com/InstanceBrick/nightcap.git
synced 2026-10-19 16:00:54 +02:00
252 lines
7.6 KiB
C++
252 lines
7.6 KiB
C++
#include "window/window_text_input_owner.h"
|
|
#include "window/main_thread_command_gate.h"
|
|
|
|
#include <gtest/gtest.h>
|
|
|
|
#include <atomic>
|
|
#include <thread>
|
|
#include <vector>
|
|
|
|
namespace mocktail {
|
|
namespace window {
|
|
namespace {
|
|
|
|
class RecordingBackend final : public TextInputBackend {
|
|
public:
|
|
bool SetArea(const TextInputArea& area) override {
|
|
calls.push_back('A');
|
|
areas.push_back(area);
|
|
return set_area_result;
|
|
}
|
|
bool ClearArea() override {
|
|
calls.push_back('C');
|
|
return true;
|
|
}
|
|
bool Start(const TextInputOptions& options) override {
|
|
calls.push_back('S');
|
|
start_options.push_back(options);
|
|
return start_result;
|
|
}
|
|
bool Stop() override {
|
|
calls.push_back('T');
|
|
return true;
|
|
}
|
|
void FlushTextEvents() override { calls.push_back('F'); }
|
|
|
|
bool set_area_result = true;
|
|
bool start_result = true;
|
|
std::vector<char> calls;
|
|
std::vector<TextInputArea> areas;
|
|
std::vector<TextInputOptions> start_options;
|
|
};
|
|
|
|
constexpr TextInputArea kArea{10, 20, 300, 40, 25};
|
|
constexpr TextInputOptions kOptions{3, false};
|
|
|
|
TEST(WindowTextInputOwnerTest, DisabledRequestsHaveNoBackendCost) {
|
|
RecordingBackend backend;
|
|
WindowTextInputOwner owner(&backend);
|
|
|
|
EXPECT_FALSE(owner.RequestShowTextInput(1, kArea, kOptions));
|
|
EXPECT_FALSE(owner.RequestHideTextInput(1));
|
|
EXPECT_TRUE(owner.Pump());
|
|
EXPECT_TRUE(backend.calls.empty());
|
|
}
|
|
|
|
TEST(WindowTextInputOwnerTest, ShowAndHideRunOnlyWhenPumped) {
|
|
RecordingBackend backend;
|
|
WindowTextInputOwner owner(&backend);
|
|
owner.SetEnabled(true);
|
|
|
|
EXPECT_TRUE(owner.RequestShowTextInput(3, kArea, kOptions));
|
|
EXPECT_TRUE(backend.calls.empty());
|
|
EXPECT_TRUE(owner.Pump());
|
|
EXPECT_EQ(backend.calls, (std::vector<char>{'F', 'A', 'S'}));
|
|
EXPECT_TRUE(owner.active());
|
|
EXPECT_EQ(owner.active_generation(), 3u);
|
|
|
|
EXPECT_TRUE(owner.RequestHideTextInput(3));
|
|
EXPECT_TRUE(owner.Pump());
|
|
EXPECT_EQ(backend.calls,
|
|
(std::vector<char>{'F', 'A', 'S', 'T', 'C', 'F'}));
|
|
EXPECT_FALSE(owner.active());
|
|
}
|
|
|
|
TEST(WindowTextInputOwnerTest, RejectsStaleFocusCommands) {
|
|
RecordingBackend backend;
|
|
WindowTextInputOwner owner(&backend);
|
|
owner.SetEnabled(true);
|
|
|
|
EXPECT_TRUE(owner.RequestShowTextInput(5, kArea, kOptions));
|
|
EXPECT_FALSE(owner.RequestShowTextInput(4, kArea, kOptions));
|
|
EXPECT_FALSE(owner.RequestHideTextInput(4));
|
|
EXPECT_TRUE(owner.Pump());
|
|
EXPECT_TRUE(owner.active());
|
|
}
|
|
|
|
TEST(WindowTextInputOwnerTest, CoalescesCommandsToLatestDesiredState) {
|
|
RecordingBackend backend;
|
|
WindowTextInputOwner owner(&backend);
|
|
owner.SetEnabled(true);
|
|
|
|
EXPECT_TRUE(owner.RequestShowTextInput(7, kArea, kOptions));
|
|
EXPECT_TRUE(owner.RequestHideTextInput(7));
|
|
EXPECT_TRUE(owner.Pump());
|
|
EXPECT_TRUE(backend.calls.empty());
|
|
}
|
|
|
|
TEST(WindowTextInputOwnerTest, ReappliesAreaAfterResizeWithoutRestart) {
|
|
RecordingBackend backend;
|
|
WindowTextInputOwner owner(&backend);
|
|
owner.SetEnabled(true);
|
|
ASSERT_TRUE(owner.RequestShowTextInput(2, kArea, kOptions));
|
|
ASSERT_TRUE(owner.Pump());
|
|
|
|
EXPECT_TRUE(owner.OnViewportChanged());
|
|
EXPECT_EQ(backend.calls, (std::vector<char>{'F', 'A', 'S', 'A'}));
|
|
EXPECT_EQ(backend.areas.size(), 2u);
|
|
}
|
|
|
|
TEST(WindowTextInputOwnerTest, CoalescedNewerHideStopsThePreviousEditor) {
|
|
RecordingBackend backend;
|
|
WindowTextInputOwner owner(&backend);
|
|
owner.SetEnabled(true);
|
|
ASSERT_TRUE(owner.RequestShowTextInput(3, kArea, kOptions));
|
|
ASSERT_TRUE(owner.Pump());
|
|
ASSERT_TRUE(owner.active());
|
|
|
|
// Closing Roblox chat hides the current editor, then briefly focuses and
|
|
// hides another TextBox before the next SDL pump.
|
|
ASSERT_TRUE(owner.RequestHideTextInput(3));
|
|
ASSERT_TRUE(owner.RequestShowTextInput(4, {}, kOptions));
|
|
ASSERT_TRUE(owner.RequestHideTextInput(4));
|
|
ASSERT_TRUE(owner.Pump());
|
|
EXPECT_FALSE(owner.active());
|
|
EXPECT_EQ(owner.active_generation(), 0u);
|
|
EXPECT_EQ(backend.calls,
|
|
(std::vector<char>{'F', 'A', 'S', 'T', 'C', 'F'}));
|
|
|
|
ASSERT_TRUE(owner.RequestShowTextInput(5, kArea, kOptions));
|
|
ASSERT_TRUE(owner.Pump());
|
|
EXPECT_TRUE(owner.active());
|
|
EXPECT_EQ(owner.active_generation(), 5u);
|
|
EXPECT_FALSE(owner.RequestHideTextInput(4));
|
|
EXPECT_TRUE(owner.Pump());
|
|
EXPECT_TRUE(owner.active());
|
|
}
|
|
|
|
TEST(WindowTextInputOwnerTest, FocusLossAndShutdownClearInputState) {
|
|
RecordingBackend backend;
|
|
WindowTextInputOwner owner(&backend);
|
|
owner.SetEnabled(true);
|
|
ASSERT_TRUE(owner.RequestShowTextInput(1, kArea, kOptions));
|
|
ASSERT_TRUE(owner.Pump());
|
|
|
|
owner.OnFocusLost();
|
|
EXPECT_FALSE(owner.active());
|
|
EXPECT_EQ(backend.calls,
|
|
(std::vector<char>{'F', 'A', 'S', 'T', 'C', 'F'}));
|
|
|
|
EXPECT_TRUE(owner.RequestShowTextInput(1, kArea, kOptions));
|
|
EXPECT_TRUE(owner.Pump());
|
|
EXPECT_FALSE(owner.active());
|
|
owner.OnFocusGained();
|
|
EXPECT_TRUE(owner.RequestShowTextInput(1, kArea, kOptions));
|
|
ASSERT_TRUE(owner.Pump());
|
|
owner.Shutdown();
|
|
EXPECT_FALSE(owner.active());
|
|
EXPECT_FALSE(owner.RequestShowTextInput(1, kArea, kOptions));
|
|
}
|
|
|
|
TEST(WindowTextInputOwnerTest, FailedStartDoesNotPublishActiveState) {
|
|
RecordingBackend backend;
|
|
backend.start_result = false;
|
|
WindowTextInputOwner owner(&backend);
|
|
owner.SetEnabled(true);
|
|
|
|
ASSERT_TRUE(owner.RequestShowTextInput(1, kArea, kOptions));
|
|
EXPECT_FALSE(owner.Pump());
|
|
EXPECT_FALSE(owner.active());
|
|
EXPECT_EQ(backend.calls, (std::vector<char>{'F', 'A', 'S', 'C'}));
|
|
}
|
|
|
|
TEST(WindowTextInputOwnerTest, GuestRequestsAreThreadSafeAndBounded) {
|
|
RecordingBackend backend;
|
|
WindowTextInputOwner owner(&backend);
|
|
owner.SetEnabled(true);
|
|
std::atomic<bool> start{false};
|
|
std::vector<std::thread> threads;
|
|
for (int thread_index = 0; thread_index < 4; ++thread_index) {
|
|
threads.emplace_back([&owner, &start, thread_index]() {
|
|
while (!start.load(std::memory_order_acquire)) {
|
|
}
|
|
for (int index = 0; index < 1000; ++index) {
|
|
TextInputArea area = kArea;
|
|
area.x = thread_index;
|
|
owner.RequestShowTextInput(9, area, kOptions);
|
|
}
|
|
});
|
|
}
|
|
start.store(true, std::memory_order_release);
|
|
for (std::thread& thread : threads) {
|
|
thread.join();
|
|
}
|
|
|
|
ASSERT_TRUE(owner.Pump());
|
|
EXPECT_EQ(backend.calls, (std::vector<char>{'F', 'A', 'S'}));
|
|
EXPECT_EQ(backend.areas.size(), 1u);
|
|
}
|
|
|
|
TEST(WindowTextInputOwnerTest, UsesSafeAreaAndPassesInputOptions) {
|
|
RecordingBackend backend;
|
|
WindowTextInputOwner owner(&backend);
|
|
owner.SetEnabled(true);
|
|
const TextInputOptions options{5, true};
|
|
|
|
ASSERT_TRUE(owner.RequestShowTextInput(8, {}, options));
|
|
ASSERT_TRUE(owner.Pump());
|
|
ASSERT_EQ(backend.areas.size(), 1u);
|
|
EXPECT_TRUE(backend.areas[0].valid());
|
|
ASSERT_EQ(backend.start_options.size(), 1u);
|
|
EXPECT_EQ(backend.start_options[0].roblox_text_input_type, 5);
|
|
EXPECT_TRUE(backend.start_options[0].multiline);
|
|
}
|
|
|
|
TEST(WindowTextInputOwnerTest, OlderHideCannotReplaceNewerPendingShow) {
|
|
RecordingBackend backend;
|
|
WindowTextInputOwner owner(&backend);
|
|
owner.SetEnabled(true);
|
|
|
|
ASSERT_TRUE(owner.RequestShowTextInput(11, kArea, kOptions));
|
|
EXPECT_FALSE(owner.RequestHideTextInput(10));
|
|
EXPECT_TRUE(owner.Pump());
|
|
EXPECT_TRUE(owner.active());
|
|
EXPECT_EQ(owner.active_generation(), 11u);
|
|
}
|
|
|
|
bool Increment(void* context) {
|
|
++*static_cast<int*>(context);
|
|
return true;
|
|
}
|
|
|
|
TEST(MainThreadCommandGateTest, DefaultPathAndRegistrationAreDeterministic) {
|
|
MainThreadCommandGate gate;
|
|
int calls = 0;
|
|
|
|
EXPECT_TRUE(gate.Invoke());
|
|
EXPECT_FALSE(gate.Register(&Increment, &calls));
|
|
gate.Activate();
|
|
ASSERT_TRUE(gate.Register(&Increment, &calls));
|
|
EXPECT_FALSE(gate.Register(&Increment, &calls));
|
|
EXPECT_TRUE(gate.Invoke());
|
|
EXPECT_EQ(calls, 1);
|
|
gate.Clear();
|
|
EXPECT_TRUE(gate.Invoke());
|
|
EXPECT_EQ(calls, 1);
|
|
gate.Deactivate();
|
|
}
|
|
|
|
} // namespace
|
|
} // namespace window
|
|
} // namespace mocktail
|