Files

1986 lines
77 KiB
C++

#include "CppUnitTest.h"
#include "StellarX.h"
#include "SxEventLoopInternal.h"
#include "SxKeyboardRouterInternal.h"
#include "SxNativeInputInternal.h"
#include "SxTextInternal.h"
#include <atomic>
#include <filesystem>
#include <fstream>
#include <string>
#include <thread>
#include <vector>
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
namespace
{
// 轻量测试控件只实现生命周期和事件所需的最小行为,避免把所有权测试绑定到真实绘图。
class FakeControl final : public Control
{
public:
FakeControl() : Control(0, 0, 10, 10) { id = "Fake"; }
void draw() override { dirty = false; }
bool handleEvent(const ExMessage&) override { return false; }
bool model() const override { return false; }
};
class FocusTestControl final : public Control
{
bool focusEnabled = true;
std::function<void(bool, StellarX::FocusReason)> focusCallback;
public:
explicit FocusTestControl(int tabOrder = 0) : Control(0, 0, 10, 10)
{
id = "FocusTest";
focusPolicy = StellarX::FocusPolicy::Programmatic |
StellarX::FocusPolicy::Pointer | StellarX::FocusPolicy::Tab;
tabIndex = tabOrder;
}
void draw() override { dirty = false; }
bool handleEvent(const ExMessage&) override { return false; }
bool model() const override { return false; }
void setFocusEnabledForTest(bool value)
{
focusEnabled = value;
notifyFocusSubtreeStateChanged();
}
void setFocusCallback(std::function<void(bool, StellarX::FocusReason)> callback)
{
focusCallback = std::move(callback);
}
protected:
bool isFocusEnabled() const override { return focusEnabled; }
void onLogicalFocusChanged(bool focused, StellarX::FocusReason reason) override
{
if (focusCallback) focusCallback(focused, reason);
}
};
class KeyRouteTestControl final : public Control
{
std::function<StellarX::KeyRouteResult(StellarX::RoutedKeyEvent&)> routeCallback;
public:
StellarX::FocusTargetId seenTarget{};
int keyboardSuspensions = 0;
KeyRouteTestControl()
{
id = "KeyRouteTest";
focusPolicy = StellarX::FocusPolicy::Programmatic |
StellarX::FocusPolicy::Pointer | StellarX::FocusPolicy::Tab;
}
void draw() override { dirty = false; }
bool handleEvent(const ExMessage&) override { return false; }
bool model() const override { return false; }
void setRouteCallback(
std::function<StellarX::KeyRouteResult(StellarX::RoutedKeyEvent&)> callback)
{
routeCallback = std::move(callback);
}
StellarX::KeyRouteResult handleKeyEvent(StellarX::RoutedKeyEvent& event) override
{
seenTarget = event.currentTarget;
return routeCallback ? routeCallback(event) : StellarX::KeyRouteResult{ true, true };
}
protected:
void onKeyboardRoutingSuspended() override { ++keyboardSuspensions; }
};
class KeyRouteTestCanvas final : public Canvas
{
std::function<StellarX::KeyRouteResult(StellarX::RoutedKeyEvent&)> routeCallback;
public:
int keyboardSuspensions = 0;
KeyRouteTestCanvas() : Canvas(0, 0, 100, 100) {}
void setRouteCallback(
std::function<StellarX::KeyRouteResult(StellarX::RoutedKeyEvent&)> callback)
{
routeCallback = std::move(callback);
}
StellarX::KeyRouteResult handleKeyEvent(StellarX::RoutedKeyEvent& event) override
{
return routeCallback ? routeCallback(event) : StellarX::KeyRouteResult{};
}
protected:
void onKeyboardRoutingSuspended() override { ++keyboardSuspensions; }
};
StellarX::NativeInputEvent MakeKeyInput(std::uint64_t sequence, UINT virtualKey = VK_SPACE)
{
StellarX::NativeInputEvent native;
native.sequence = sequence;
native.messageTime = static_cast<DWORD>(sequence * 10);
native.activationAtCapture = { true, 1 };
StellarX::KeyEvent key;
key.action = StellarX::KeyAction::Down;
key.virtualKey = virtualKey;
native.payload = key;
return native;
}
StellarX::Internal::KeyboardRoutingEnvironment ActiveKeyboardEnvironment(
std::uint64_t generation = 1)
{
StellarX::Internal::KeyboardRoutingEnvironment environment;
environment.currentActivation = { true, generation };
return environment;
}
StellarX::Internal::KeyboardRouteDispatchResult RouteKeyboard(
Window& window, const StellarX::NativeInputEvent& event,
const StellarX::Internal::KeyboardRoutingEnvironment& environment =
ActiveKeyboardEnvironment())
{
return StellarX::Internal::KeyboardRouter::route(window, event, environment);
}
std::filesystem::path MakeTempLogPath(const char* name)
{
// 使用进程号隔离并行测试产生的日志路径;测试结束后由用例主动清理。
return std::filesystem::temp_directory_path() /
(std::string("stellarx-") + name + "-" + std::to_string(GetCurrentProcessId()) + ".log");
}
}
namespace StellarXNativeTests
{
// 验证句柄身份和回调期间的延迟变更不会破坏容器遍历或对象生命周期。
TEST_CLASS(OwnershipTests)
{
public:
TEST_METHOD(HandlesAreUnique)
{
// 句柄必须与对象地址解耦,才能在对象被销毁后安全地表示原对象身份。
auto first = std::make_unique<FakeControl>();
auto second = std::make_unique<FakeControl>();
Assert::AreNotEqual(first->getHandle().value, second->getHandle().value);
}
TEST_METHOD(CallbackCanRemoveCurrentControlSafely)
{
// 回调删除自身后,当前事件栈仍会继续返回,但不能再次访问已释放控件。
Window window(200, 120, 1);
auto button = std::make_unique<Button>(10, 10, 80, 30, "remove");
Button* raw = button.get();
const auto handle = window.addControl(std::move(button));
raw->setOnClickListener([&]() { Assert::IsTrue(window.removeControl(handle)); });
ExMessage down{}; down.message = WM_LBUTTONDOWN; down.x = 20; down.y = 20;
ExMessage up = down; up.message = WM_LBUTTONUP;
raw->handleEvent(down);
raw->handleEvent(up);
Assert::IsNull(window.findControl(handle));
Assert::AreEqual<size_t>(0, window.getControlHandles().size());
}
TEST_METHOD(CallbackCanQueueAddAndClear)
{
// 同一回调内先添加再清空,提交顺序必须确定且不得让遍历迭代器失效。
Window window(200, 120, 1);
auto button = std::make_unique<Button>(10, 10, 80, 30, "mutate");
Button* raw = button.get();
window.addControl(std::move(button));
raw->setOnClickListener([&]() {
window.addControl(std::make_unique<FakeControl>());
window.clearControls();
});
ExMessage down{}; down.message = WM_LBUTTONDOWN; down.x = 20; down.y = 20;
ExMessage up = down; up.message = WM_LBUTTONUP;
raw->handleEvent(down);
raw->handleEvent(up);
Assert::AreEqual<size_t>(0, window.getControlHandles().size());
}
TEST_METHOD(CallbackCanRemoveSiblingAndLeaveRemainingControlUsable)
{
// 删除兄弟控件后,未受影响的后续控件仍应能够接收下一次点击。
Window window(200, 120, 1);
auto remover = std::make_unique<Button>(10, 10, 80, 30, "remove sibling");
Button* removerRaw = remover.get();
window.addControl(std::move(remover));
auto sibling = std::make_unique<FakeControl>();
const auto siblingHandle = window.addControl(std::move(sibling));
int remainingClicks = 0;
auto remaining = std::make_unique<Button>(100, 10, 80, 30, "remaining");
Button* remainingRaw = remaining.get();
remainingRaw->setOnClickListener([&]() { ++remainingClicks; });
window.addControl(std::move(remaining));
removerRaw->setOnClickListener([&]() { window.removeControl(siblingHandle); });
ExMessage down{}; down.message = WM_LBUTTONDOWN; down.x = 20; down.y = 20;
ExMessage up = down; up.message = WM_LBUTTONUP;
removerRaw->handleEvent(down);
removerRaw->handleEvent(up);
Assert::IsNull(window.findControl(siblingHandle));
down.x = 120; up.x = 120;
remainingRaw->handleEvent(down);
remainingRaw->handleEvent(up);
Assert::AreEqual(1, remainingClicks);
}
TEST_METHOD(QueuedAddCanBeRemovedBeforeTransactionCommit)
{
// 队列中的新对象也必须拥有可撤销的句柄,提交前移除不能误伤现有对象。
Window window(200, 120, 1);
auto canvas = std::make_unique<Canvas>(0, 50, 100, 60);
Canvas* canvasRaw = canvas.get();
window.addControl(std::move(canvas));
auto trigger = std::make_unique<Button>(10, 10, 80, 30, "queue");
Button* triggerRaw = trigger.get();
window.addControl(std::move(trigger));
triggerRaw->setOnClickListener([&]() {
const auto windowHandle = window.addControl(std::make_unique<FakeControl>());
Assert::IsTrue(window.removeControl(windowHandle));
const auto canvasHandle = canvasRaw->addControl(std::make_unique<FakeControl>());
Assert::IsTrue(canvasRaw->removeControl(canvasHandle));
});
ExMessage down{}; down.message = WM_LBUTTONDOWN; down.x = 20; down.y = 20;
ExMessage up = down; up.message = WM_LBUTTONUP;
triggerRaw->handleEvent(down);
triggerRaw->handleEvent(up);
Assert::AreEqual<size_t>(2, window.getControlHandles().size());
Assert::AreEqual<size_t>(0, canvasRaw->getControlHandles().size());
}
TEST_METHOD(TabAddedFromCallbackCommitsWithPendingPageControl)
{
// 页签及其页面控件在同一事务中加入时,页面内部的排队变更也必须一并提交。
Window window(300, 200, 1);
auto tabs = std::make_unique<TabControl>(0, 50, 200, 140);
TabControl* tabsRaw = tabs.get();
tabsRaw->addTab(std::make_unique<Button>(0, 0, 80, 24, "first"),
std::make_unique<Canvas>(0, 0, 200, 110));
window.addControl(std::move(tabs));
auto trigger = std::make_unique<Button>(10, 10, 80, 30, "add tab");
Button* triggerRaw = trigger.get();
window.addControl(std::move(trigger));
triggerRaw->setOnClickListener([&]() {
const size_t index = tabsRaw->addTab(
std::make_unique<Button>(0, 0, 80, 24, "second"),
std::make_unique<Canvas>(0, 0, 200, 110));
auto rejected = tabsRaw->tryAddToPage(index, std::make_unique<FakeControl>());
Assert::IsNull(rejected.get());
});
ExMessage down{}; down.message = WM_LBUTTONDOWN; down.x = 20; down.y = 20;
ExMessage up = down; up.message = WM_LBUTTONUP;
triggerRaw->handleEvent(down);
triggerRaw->handleEvent(up);
Assert::AreEqual(2, tabsRaw->count());
}
};
TEST_CLASS(FocusManagerTests)
{
public:
TEST_METHOD(NestedControlResolvesByFocusTargetId)
{
Window window(200, 120, 1);
auto canvas = std::make_unique<Canvas>(0, 0, 100, 100);
auto child = std::make_unique<FocusTestControl>();
FocusTestControl* childRaw = child.get();
const auto childHandle = canvas->addControl(std::move(child));
window.addControl(std::move(canvas));
const auto result = window.getFocusManager().requestFocus({ childHandle, 0 });
Assert::AreEqual(static_cast<int>(StellarX::FocusError::None),
static_cast<int>(result.error));
Assert::IsTrue(childRaw->hasLogicalFocus());
Assert::AreEqual(childHandle.value,
window.getFocusManager().focusedTarget().owner.value);
const auto stops = window.getFocusManager().collectTabStops();
Assert::AreEqual<size_t>(1, stops.size());
Assert::AreEqual(childHandle.value, stops.front().id.owner.value);
Assert::AreEqual<std::uint32_t>(0, stops.front().id.subNode);
}
TEST_METHOD(PointerDownMovesLogicalFocusWithoutKeyboardRouting)
{
Window window(240, 120, 1);
auto button = std::make_unique<Button>(10, 10, 80, 30, "button");
Button* buttonRaw = button.get();
window.addControl(std::move(button));
auto text = std::make_unique<TextBox>(110, 10, 100, 30, "text");
TextBox* textRaw = text.get();
window.addControl(std::move(text));
ExMessage down{}; down.message = WM_LBUTTONDOWN; down.x = 20; down.y = 20;
Assert::IsTrue(buttonRaw->handleEvent(down));
Assert::IsTrue(buttonRaw->hasLogicalFocus());
down.x = 120;
Assert::IsTrue(textRaw->handleEvent(down));
Assert::IsFalse(buttonRaw->hasLogicalFocus());
Assert::IsTrue(textRaw->hasLogicalFocus());
}
TEST_METHOD(CanvasBackgroundPointerDownClearsFocusAndConsumes)
{
Window window(240, 160, 1);
auto canvas = std::make_unique<Canvas>(20, 20, 160, 120);
Canvas* canvasRaw = canvas.get();
auto button = std::make_unique<Button>(10, 10, 80, 30, "button");
Button* buttonRaw = button.get();
const auto buttonHandle = canvas->addControl(std::move(button));
window.addControl(std::move(canvas));
window.getFocusManager().requestFocus({ buttonHandle, 0 });
Assert::IsTrue(buttonRaw->hasLogicalFocus());
ExMessage down{};
down.message = WM_LBUTTONDOWN;
down.x = 160;
down.y = 120;
Assert::IsTrue(canvasRaw->handleEvent(down));
Assert::IsFalse(buttonRaw->hasLogicalFocus());
Assert::IsFalse(static_cast<bool>(window.getFocusManager().focusedTarget()));
}
TEST_METHOD(FocusLostCallbackCanDeleteNewTarget)
{
Window window(200, 120, 1);
auto oldControl = std::make_unique<FocusTestControl>();
FocusTestControl* oldRaw = oldControl.get();
const auto oldHandle = window.addControl(std::move(oldControl));
auto newControl = std::make_unique<FocusTestControl>();
const auto newHandle = window.addControl(std::move(newControl));
window.getFocusManager().requestFocus({ oldHandle, 0 });
oldRaw->setFocusCallback([&](bool focused, StellarX::FocusReason)
{
if (!focused) Assert::IsTrue(window.removeControl(newHandle));
});
const auto result = window.getFocusManager().requestFocus({ newHandle, 0 });
Assert::AreEqual(static_cast<int>(StellarX::FocusError::InvalidTarget),
static_cast<int>(result.error));
Assert::IsFalse(static_cast<bool>(window.getFocusManager().focusedTarget()));
Assert::IsNull(window.findControl(newHandle));
Assert::IsFalse(oldRaw->hasLogicalFocus());
}
TEST_METHOD(FocusLostCallbackForcesFreshEnabledSnapshot)
{
Window window(200, 120, 1);
auto oldControl = std::make_unique<FocusTestControl>();
FocusTestControl* oldRaw = oldControl.get();
const auto oldHandle = window.addControl(std::move(oldControl));
auto newControl = std::make_unique<FocusTestControl>();
FocusTestControl* newRaw = newControl.get();
const auto newHandle = window.addControl(std::move(newControl));
window.getFocusManager().requestFocus({ oldHandle, 0 });
oldRaw->setFocusCallback([&](bool focused, StellarX::FocusReason)
{
if (!focused) newRaw->setFocusEnabledForTest(false);
});
const auto result = window.getFocusManager().requestFocus({ newHandle, 0 });
Assert::AreEqual(static_cast<int>(StellarX::FocusError::Disabled),
static_cast<int>(result.error));
Assert::IsFalse(newRaw->hasLogicalFocus());
Assert::IsFalse(static_cast<bool>(window.getFocusManager().focusedTarget()));
}
TEST_METHOD(FocusGainedCallbackCanDeleteItselfAndFallsBack)
{
Window window(200, 120, 1);
auto fallback = std::make_unique<FocusTestControl>();
FocusTestControl* fallbackRaw = fallback.get();
const auto fallbackHandle = window.addControl(std::move(fallback));
auto selfDeleting = std::make_unique<FocusTestControl>();
FocusTestControl* deletingRaw = selfDeleting.get();
const auto deletingHandle = window.addControl(std::move(selfDeleting));
window.getFocusManager().requestFocus({ fallbackHandle, 0 });
deletingRaw->setFocusCallback([&](bool focused, StellarX::FocusReason)
{
if (focused) Assert::IsTrue(window.removeControl(deletingHandle));
});
const auto result = window.getFocusManager().requestFocus({ deletingHandle, 0 });
Assert::AreEqual(static_cast<int>(StellarX::FocusError::None),
static_cast<int>(result.error));
Assert::AreEqual(fallbackHandle.value,
window.getFocusManager().focusedTarget().owner.value);
Assert::IsTrue(fallbackRaw->hasLogicalFocus());
Assert::IsNull(window.findControl(deletingHandle));
}
TEST_METHOD(ReentrantPendingIsSingleSlotAndDoesNotReviveOlderRequest)
{
Window window(300, 120, 1);
auto oldControl = std::make_unique<FocusTestControl>();
FocusTestControl* oldRaw = oldControl.get();
const auto oldHandle = window.addControl(std::move(oldControl));
auto trigger = std::make_unique<FocusTestControl>();
const auto triggerHandle = window.addControl(std::move(trigger));
auto olderPending = std::make_unique<FocusTestControl>();
FocusTestControl* olderRaw = olderPending.get();
const auto olderHandle = window.addControl(std::move(olderPending));
auto latestPending = std::make_unique<FocusTestControl>();
const auto latestHandle = window.addControl(std::move(latestPending));
window.getFocusManager().requestFocus({ oldHandle, 0 });
oldRaw->setFocusCallback([&](bool focused, StellarX::FocusReason)
{
if (focused) return;
window.getFocusManager().requestFocus({ olderHandle, 0 });
window.getFocusManager().requestFocus({ latestHandle, 0 });
Assert::IsTrue(window.removeControl(latestHandle));
});
window.getFocusManager().requestFocus({ triggerHandle, 0 });
Assert::AreEqual(triggerHandle.value,
window.getFocusManager().focusedTarget().owner.value);
Assert::IsFalse(olderRaw->hasLogicalFocus());
Assert::IsNull(window.findControl(latestHandle));
}
TEST_METHOD(FocusCallbackCanPushScopeAndPopRestoresOuterFocus)
{
Window window(300, 160, 1);
auto scopeRoot = std::make_unique<Canvas>(0, 40, 200, 100);
const auto scopeRootHandle = scopeRoot->getHandle();
auto scopedChild = std::make_unique<FocusTestControl>();
FocusTestControl* childRaw = scopedChild.get();
const auto childHandle = scopeRoot->addControl(std::move(scopedChild));
window.addControl(std::move(scopeRoot));
auto outer = std::make_unique<FocusTestControl>();
FocusTestControl* outerRaw = outer.get();
const auto outerHandle = window.addControl(std::move(outer));
StellarX::FocusScopeId pushed{};
bool didPush = false;
outerRaw->setFocusCallback([&](bool focused, StellarX::FocusReason)
{
if (focused && !didPush)
{
didPush = true;
pushed = window.getFocusManager().pushScope(
{ scopeRootHandle, 0 }, { childHandle, 0 });
}
});
window.getFocusManager().requestFocus({ outerHandle, 0 });
Assert::IsTrue(static_cast<bool>(pushed));
Assert::AreEqual(childHandle.value,
window.getFocusManager().focusedTarget().owner.value);
Assert::IsTrue(childRaw->hasLogicalFocus());
Assert::IsTrue(window.getFocusManager().popScope(pushed));
Assert::AreEqual(outerHandle.value,
window.getFocusManager().focusedTarget().owner.value);
Assert::IsTrue(outerRaw->hasLogicalFocus());
}
TEST_METHOD(FocusLostCallbackScopeRestoresTheActualOldTarget)
{
Window window(360, 180, 1);
auto unrelated = std::make_unique<FocusTestControl>();
const auto unrelatedHandle = window.addControl(std::move(unrelated));
auto oldControl = std::make_unique<FocusTestControl>();
FocusTestControl* oldRaw = oldControl.get();
const auto oldHandle = window.addControl(std::move(oldControl));
auto requested = std::make_unique<FocusTestControl>();
const auto requestedHandle = window.addControl(std::move(requested));
auto scopeRoot = std::make_unique<Canvas>(0, 40, 200, 100);
const auto scopeRootHandle = scopeRoot->getHandle();
auto scopedChild = std::make_unique<FocusTestControl>();
const auto childHandle = scopeRoot->addControl(std::move(scopedChild));
window.addControl(std::move(scopeRoot));
StellarX::FocusScopeId pushed{};
oldRaw->setFocusCallback([&](bool focused, StellarX::FocusReason)
{
if (!focused && !pushed)
pushed = window.getFocusManager().pushScope(
{ scopeRootHandle, 0 }, { childHandle, 0 });
});
window.getFocusManager().requestFocus({ oldHandle, 0 });
window.getFocusManager().requestFocus({ requestedHandle, 0 });
Assert::IsTrue(static_cast<bool>(pushed));
Assert::AreEqual(childHandle.value,
window.getFocusManager().focusedTarget().owner.value);
Assert::IsTrue(window.getFocusManager().popScope(pushed));
Assert::AreEqual(oldHandle.value,
window.getFocusManager().focusedTarget().owner.value);
Assert::AreNotEqual(unrelatedHandle.value,
window.getFocusManager().focusedTarget().owner.value);
}
TEST_METHOD(ReentrantFocusCycleStopsAtExplicitLimit)
{
Window window(200, 120, 1);
auto first = std::make_unique<FocusTestControl>();
FocusTestControl* firstRaw = first.get();
const auto firstHandle = window.addControl(std::move(first));
auto second = std::make_unique<FocusTestControl>();
FocusTestControl* secondRaw = second.get();
const auto secondHandle = window.addControl(std::move(second));
firstRaw->setFocusCallback([&](bool focused, StellarX::FocusReason)
{
if (focused) window.getFocusManager().requestFocus({ secondHandle, 0 });
});
secondRaw->setFocusCallback([&](bool focused, StellarX::FocusReason)
{
if (focused) window.getFocusManager().requestFocus({ firstHandle, 0 });
});
const auto result = window.getFocusManager().requestFocus({ firstHandle, 0 });
Assert::AreEqual(static_cast<int>(StellarX::FocusError::ReentrancyLimit),
static_cast<int>(result.error));
Assert::AreEqual(static_cast<int>(StellarX::FocusError::ReentrancyLimit),
static_cast<int>(window.getFocusManager().getLastError()));
Assert::IsTrue(static_cast<bool>(window.getFocusManager().focusedTarget()));
}
TEST_METHOD(ActiveRemovalUsesNextTabStopFallback)
{
Window window(240, 120, 1);
auto first = std::make_unique<FocusTestControl>(0);
const auto firstHandle = window.addControl(std::move(first));
auto second = std::make_unique<FocusTestControl>(1);
const auto secondHandle = window.addControl(std::move(second));
auto third = std::make_unique<FocusTestControl>(2);
FocusTestControl* thirdRaw = third.get();
const auto thirdHandle = window.addControl(std::move(third));
window.getFocusManager().requestFocus({ secondHandle, 0 });
Assert::IsTrue(window.removeControl(secondHandle));
Assert::AreEqual(thirdHandle.value,
window.getFocusManager().focusedTarget().owner.value);
Assert::IsTrue(thirdRaw->hasLogicalFocus());
Assert::IsNotNull(window.findControl(firstHandle));
}
TEST_METHOD(ClearInvalidatesWholeBatchBeforeChoosingFallback)
{
Window window(240, 120, 1);
auto first = std::make_unique<FocusTestControl>();
const auto firstHandle = window.addControl(std::move(first));
int unexpectedGains = 0;
auto second = std::make_unique<FocusTestControl>();
second->setFocusCallback([&](bool focused, StellarX::FocusReason)
{ if (focused) ++unexpectedGains; });
window.addControl(std::move(second));
auto third = std::make_unique<FocusTestControl>();
third->setFocusCallback([&](bool focused, StellarX::FocusReason)
{ if (focused) ++unexpectedGains; });
window.addControl(std::move(third));
window.getFocusManager().requestFocus({ firstHandle, 0 });
window.clearControls();
Assert::AreEqual(0, unexpectedGains);
Assert::IsFalse(static_cast<bool>(window.getFocusManager().focusedTarget()));
Assert::AreEqual<size_t>(0, window.getControlHandles().size());
}
TEST_METHOD(TabControlExposesRealButtonHandleWithoutSyntheticSubNode)
{
Window window(300, 200, 1);
auto tabs = std::make_unique<TabControl>(0, 0, 300, 200);
auto tabButton = std::make_unique<Button>(0, 0, 80, 24, "tab");
const auto tabHandle = tabButton->getHandle();
tabs->addTab(std::move(tabButton), std::make_unique<Canvas>(0, 24, 300, 176));
window.addControl(std::move(tabs));
const auto stops = window.getFocusManager().collectTabStops();
auto found = std::find_if(stops.begin(), stops.end(), [&](const auto& item)
{ return item.id.owner == tabHandle; });
Assert::IsTrue(found != stops.end());
Assert::AreEqual<std::uint32_t>(0, found->id.subNode);
}
TEST_METHOD(TabControlExcludesInactivePageFromFocusTree)
{
Window window(320, 220, 1);
auto tabs = std::make_unique<TabControl>(0, 0, 300, 200);
auto firstPage = std::make_unique<Canvas>(0, 24, 300, 176);
Canvas* firstPageRaw = firstPage.get();
auto firstChild = std::make_unique<FocusTestControl>();
const auto firstChildHandle = firstPage->addControl(std::move(firstChild));
tabs->addTab(std::make_unique<Button>(0, 0, 80, 24, "first"),
std::move(firstPage));
auto secondPage = std::make_unique<Canvas>(0, 24, 300, 176);
Canvas* secondPageRaw = secondPage.get();
auto secondChild = std::make_unique<FocusTestControl>();
const auto secondChildHandle = secondPage->addControl(std::move(secondChild));
tabs->addTab(std::make_unique<Button>(80, 0, 80, 24, "second"),
std::move(secondPage));
window.addControl(std::move(tabs));
auto contains = [&](StellarX::ControlHandle handle)
{
const auto stops = window.getFocusManager().collectTabStops();
return std::any_of(stops.begin(), stops.end(), [&](const auto& item)
{ return item.id.owner == handle; });
};
// addTab keeps pages hidden until the first real draw. Drive visibility directly here so
// this non-GDI unit isolates the focus-tree filter rather than TabControl rendering.
firstPageRaw->setIsVisible(true);
Assert::IsTrue(contains(firstChildHandle));
Assert::IsFalse(contains(secondChildHandle));
window.getFocusManager().requestFocus({ firstChildHandle, 0 });
firstPageRaw->setIsVisible(false);
secondPageRaw->setIsVisible(true);
Assert::IsFalse(contains(firstChildHandle));
Assert::IsTrue(contains(secondChildHandle));
Assert::AreNotEqual(firstChildHandle.value,
window.getFocusManager().focusedTarget().owner.value);
}
};
// 验证 MBCS 文本所有权、空页签边界、提示延时和密码掩码等纯逻辑行为。
TEST_CLASS(TextAndTabTests)
{
public:
TEST_METHOD(ButtonConstructsWithChineseTextStyle)
{
// Button 的字体样式含有 std::string;构造中文按钮可回归成员初始化顺序错误。
Button button(10, 10, 80, 30, "圆");
Assert::AreEqual(std::string("圆"), button.getButtonText());
Assert::AreEqual(std::string("微软雅黑"), button.getButtonTextStyle().faceName);
}
TEST_METHOD(ControlTextOwnsAndComparesFaceName)
{
// 字体名应复制字符串内容,而不是保存临时缓冲区地址或比较地址。
StellarX::ControlText first;
first.faceName = std::string("Temporary Font");
StellarX::ControlText second = first;
Assert::IsTrue(first == second);
second.faceName = "Other Font";
Assert::IsTrue(first != second);
Assert::AreEqual(std::string("Temporary Font"), first.faceName);
}
TEST_METHOD(TabRejectsInvalidPageWithoutConsumingControl)
{
// 非法页索引必须原样返还控件所有权,空页签也不能越界访问。
TabControl tabs(0, 0, 300, 200);
Assert::IsTrue(tabs.setActiveIndex(0));
auto rejected = tabs.tryAddToPage(0, std::make_unique<FakeControl>());
Assert::IsNotNull(rejected.get());
const size_t page = tabs.addTab(
std::make_unique<Button>(0, 0, 80, 24, "same"),
std::make_unique<Canvas>(0, 0, 300, 176));
Assert::AreEqual<size_t>(0, page);
auto accepted = tabs.tryAddToPage(page, std::move(rejected));
Assert::IsNull(accepted.get());
}
TEST_METHOD(TooltipAppearsOnTickWithoutAdditionalMouseMove)
{
// 鼠标停留后的显示由时钟驱动,不应要求额外的 WM_MOUSEMOVE 才能到期。
FakeControl repaintSink;
Button button(10, 10, 80, 30, "tooltip");
button.setParent(&repaintSink);
button.enableTooltip(true);
button.setTooltipDelay(1000);
ExMessage move{}; move.message = WM_MOUSEMOVE; move.x = 20; move.y = 20;
Assert::IsTrue(button.handleEvent(move));
Assert::IsFalse(button.isTooltipVisible());
const ULONGLONG afterMove = GetTickCount64();
button.tick(afterMove);
Assert::IsFalse(button.isTooltipVisible());
button.tick(afterMove + 1000);
Assert::IsTrue(button.isTooltipVisible());
move.x = 200; move.y = 100;
Assert::IsFalse(button.handleEvent(move));
Assert::IsFalse(button.isTooltipVisible());
move.x = 20; move.y = 20;
button.handleEvent(move);
button.tick(GetTickCount64() + 1000);
Assert::IsTrue(button.isTooltipVisible());
button.enableTooltip(false);
Assert::IsFalse(button.isTooltipVisible());
}
TEST_METHOD(PasswordMaskCountsMbcCharactersWithoutOverrun)
{
// 掩码按 MBCS 逻辑字符计数,同时覆盖不完整和非法字节序列。
Assert::AreEqual(std::string("***"), StellarX::Internal::makePasswordMask("abc"));
const std::string gbk = std::string("\xD6\xD0\xCE\xC4", 4) + "A";
Assert::AreEqual(std::string("***"), StellarX::Internal::makePasswordMask(gbk));
const std::string incomplete = std::string("A\x81", 2);
Assert::AreEqual(std::string("**"), StellarX::Internal::makePasswordMask(incomplete));
const std::string invalidPair = std::string("\x81\x30", 2);
Assert::AreEqual(std::string("**"), StellarX::Internal::makePasswordMask(invalidPair));
}
};
// 验证消息批处理上限、空闲休眠策略和关闭消息的停止语义。
TEST_CLASS(EventLoopTests)
{
public:
TEST_METHOD(ThousandMessagesDrainInEightActiveRounds)
{
// 每轮最多处理固定数量消息,活动轮不休眠,1000 条消息应在 8 轮内排空。
std::vector<ExMessage> messages(1000);
size_t next = 0;
size_t handled = 0;
int rounds = 0;
int activeSleeps = 0;
ExMessage current{};
while (next < messages.size())
{
const auto batch = StellarX::Internal::drainMessageBatch(current,
[&](ExMessage& out) {
if (next >= messages.size()) return false;
out = messages[next++];
return true;
},
[&](const ExMessage&) { ++handled; return true; });
++rounds;
if (batch.shouldSleep()) ++activeSleeps;
}
Assert::AreEqual<size_t>(1000, handled);
Assert::AreEqual(8, rounds);
Assert::AreEqual(0, activeSleeps);
const auto idle = StellarX::Internal::drainMessageBatch(current,
[](ExMessage&) { return false; }, [](const ExMessage&) { return true; });
Assert::IsTrue(idle.shouldSleep());
}
TEST_METHOD(CloseStopsBatchAfterEarlierResize)
{
// 批次内遇到 WM_CLOSE 后停止继续取消息,之前的 WM_SIZE 仍必须保留。
std::vector<ExMessage> messages(3);
messages[0].message = WM_SIZE;
messages[1].message = WM_CLOSE;
messages[2].message = WM_MOUSEMOVE;
size_t next = 0;
std::vector<UINT> handled;
ExMessage current{};
const auto batch = StellarX::Internal::drainMessageBatch(current,
[&](ExMessage& out) {
if (next >= messages.size()) return false;
out = messages[next++];
return true;
},
[&](const ExMessage& message) {
handled.push_back(message.message);
return message.message != WM_CLOSE;
});
Assert::IsTrue(batch.stopped);
Assert::AreEqual<size_t>(2, batch.processed);
Assert::AreEqual<size_t>(2, handled.size());
Assert::AreEqual<UINT>(WM_SIZE, handled[0]);
Assert::AreEqual<UINT>(WM_CLOSE, handled[1]);
Assert::AreEqual<size_t>(2, next);
}
};
// These contract tests cover routing types, the default Control seam, the
// production Routed default, and the startup-only Legacy A/B gate.
TEST_CLASS(KeyboardRoutingScaffoldTests)
{
public:
TEST_METHOD(DefaultControlKeyRouteDoesNotConsumeOrPreventDefault)
{
FakeControl control;
StellarX::RoutedKeyEvent event;
event.sequence = 17;
event.key.virtualKey = VK_TAB;
event.originalTarget = { control.getHandle(), 0 };
event.currentTarget = event.originalTarget;
const auto result = control.handleKeyEvent(event);
Assert::IsFalse(result.stopBubble);
Assert::IsFalse(result.preventFrameworkDefault);
Assert::AreEqual<std::uint64_t>(17, event.sequence);
Assert::AreEqual<UINT>(VK_TAB, event.key.virtualKey);
Assert::IsTrue(event.currentTarget == event.originalTarget);
}
TEST_METHOD(PipelineDefaultsToRoutedAndCanOptIntoLegacyBeforeLock)
{
Window window(200, 120, 1);
Assert::IsTrue(
StellarX::Internal::keyboardPipelineMode(window) ==
StellarX::Internal::KeyboardPipelineMode::Routed);
Assert::IsTrue(StellarX::Internal::configureKeyboardPipelineForTesting(
window, StellarX::Internal::KeyboardPipelineMode::LegacyCompatibility));
Assert::IsTrue(
StellarX::Internal::keyboardPipelineMode(window) ==
StellarX::Internal::KeyboardPipelineMode::LegacyCompatibility);
}
TEST_METHOD(PipelineCannotChangeAfterEventLoopLock)
{
Window window(200, 120, 1);
Assert::IsTrue(StellarX::Internal::configureKeyboardPipelineForTesting(
window, StellarX::Internal::KeyboardPipelineMode::LegacyCompatibility));
StellarX::Internal::lockKeyboardPipeline(window);
Assert::IsFalse(StellarX::Internal::configureKeyboardPipelineForTesting(
window, StellarX::Internal::KeyboardPipelineMode::Routed));
Assert::IsTrue(
StellarX::Internal::keyboardPipelineMode(window) ==
StellarX::Internal::KeyboardPipelineMode::LegacyCompatibility);
}
TEST_METHOD(DerivedControlCanReturnIndependentRouteDecisions)
{
KeyRouteTestControl control;
StellarX::RoutedKeyEvent event;
event.currentTarget = { control.getHandle(), 0 };
Control& base = control;
const auto result = base.handleKeyEvent(event);
Assert::IsTrue(result.stopBubble);
Assert::IsTrue(result.preventFrameworkDefault);
Assert::IsTrue(control.seenTarget == event.currentTarget);
}
TEST_METHOD(EasyXKeyboardClassifierDoesNotSuppressMouseOrWindowMessages)
{
Assert::IsTrue(StellarX::Internal::isEasyXKeyboardMessage(WM_KEYDOWN));
Assert::IsTrue(StellarX::Internal::isEasyXKeyboardMessage(WM_CHAR));
Assert::IsFalse(StellarX::Internal::isEasyXKeyboardMessage(WM_MOUSEMOVE));
Assert::IsFalse(StellarX::Internal::isEasyXKeyboardMessage(WM_SIZE));
}
TEST_METHOD(ProductionRoutedModeSuppressesOnlyLegacyKeyboardBusinessDelivery)
{
Window window(200, 120, 1);
Assert::IsFalse(StellarX::Internal::shouldDeliverLegacyEasyXMessage(
window, WM_KEYDOWN));
Assert::IsTrue(StellarX::Internal::shouldDeliverLegacyEasyXMessage(
window, WM_MOUSEMOVE));
Assert::IsTrue(StellarX::Internal::shouldDeliverLegacyEasyXMessage(
window, WM_SIZE));
Assert::IsTrue(StellarX::Internal::configureKeyboardPipelineForTesting(
window, StellarX::Internal::KeyboardPipelineMode::LegacyCompatibility));
Assert::IsTrue(StellarX::Internal::shouldDeliverLegacyEasyXMessage(
window, WM_KEYDOWN));
}
TEST_METHOD(DialogCodeClaimsOnlyExplicitNormalTabInRoutedMode)
{
Window window(200, 120, 1);
Assert::IsTrue(StellarX::Internal::shouldAddTabDialogCode(
window, VK_TAB, 0));
Assert::IsFalse(StellarX::Internal::shouldAddTabDialogCode(
window, VK_RETURN, 0));
MSG tabDown{};
tabDown.message = WM_KEYDOWN;
tabDown.wParam = VK_TAB;
Assert::IsTrue(StellarX::Internal::shouldAddTabDialogCode(
window, VK_TAB, reinterpret_cast<LPARAM>(&tabDown)));
MSG systemTab = tabDown;
systemTab.message = WM_SYSKEYDOWN;
Assert::IsFalse(StellarX::Internal::shouldAddTabDialogCode(
window, VK_TAB, reinterpret_cast<LPARAM>(&systemTab)));
MSG mismatched = tabDown;
mismatched.wParam = VK_RETURN;
Assert::IsFalse(StellarX::Internal::shouldAddTabDialogCode(
window, VK_TAB, reinterpret_cast<LPARAM>(&mismatched)));
Assert::IsTrue(StellarX::Internal::configureKeyboardPipelineForTesting(
window, StellarX::Internal::KeyboardPipelineMode::LegacyCompatibility));
Assert::IsFalse(StellarX::Internal::shouldAddTabDialogCode(
window, VK_TAB, 0));
}
};
TEST_CLASS(KeyboardRouterTests)
{
public:
TEST_METHOD(KeyBubblesFromFocusedLeafAlongOriginalIdentityPath)
{
Window window(240, 160, 1);
std::vector<std::uint64_t> delivered;
auto canvas = std::make_unique<KeyRouteTestCanvas>();
KeyRouteTestCanvas* canvasRaw = canvas.get();
const auto canvasHandle = canvas->getHandle();
auto leaf = std::make_unique<KeyRouteTestControl>();
KeyRouteTestControl* leafRaw = leaf.get();
const auto leafHandle = canvas->addControl(std::move(leaf));
leafRaw->setRouteCallback([&](StellarX::RoutedKeyEvent& event)
{
delivered.push_back(event.currentTarget.owner.value);
Assert::AreEqual(leafHandle.value, event.originalTarget.owner.value);
Assert::AreEqual<std::uint64_t>(101, event.sequence);
Assert::AreEqual<UINT>(VK_RETURN, event.key.virtualKey);
return StellarX::KeyRouteResult{};
});
canvasRaw->setRouteCallback([&](StellarX::RoutedKeyEvent& event)
{
delivered.push_back(event.currentTarget.owner.value);
Assert::AreEqual(leafHandle.value, event.originalTarget.owner.value);
return StellarX::KeyRouteResult{};
});
window.addControl(std::move(canvas));
window.getFocusManager().requestFocus({ leafHandle, 0 });
const auto outcome = RouteKeyboard(
window, MakeKeyInput(101, VK_RETURN));
Assert::IsTrue(outcome.keyEvent);
Assert::IsTrue(outcome.pathComplete);
Assert::AreEqual<size_t>(2, outcome.pathLength);
Assert::AreEqual<size_t>(2, outcome.deliveredTargets);
Assert::AreEqual<size_t>(0, outcome.skippedTargets);
Assert::AreEqual<size_t>(2, delivered.size());
Assert::AreEqual(leafHandle.value, delivered[0]);
Assert::AreEqual(canvasHandle.value, delivered[1]);
Assert::IsTrue(leafRaw->seenTarget == StellarX::FocusTargetId{ leafHandle, 0 });
}
TEST_METHOD(StopBubbleAndPreventDefaultRemainIndependent)
{
Window window(240, 160, 1);
int parentCalls = 0;
auto canvas = std::make_unique<KeyRouteTestCanvas>();
KeyRouteTestCanvas* canvasRaw = canvas.get();
auto leaf = std::make_unique<KeyRouteTestControl>();
KeyRouteTestControl* leafRaw = leaf.get();
const auto leafHandle = canvas->addControl(std::move(leaf));
canvasRaw->setRouteCallback([&](StellarX::RoutedKeyEvent&)
{
++parentCalls;
return StellarX::KeyRouteResult{};
});
window.addControl(std::move(canvas));
window.getFocusManager().requestFocus({ leafHandle, 0 });
leafRaw->setRouteCallback([](StellarX::RoutedKeyEvent&)
{ return StellarX::KeyRouteResult{ true, false }; });
const auto stopped = RouteKeyboard(
window, MakeKeyInput(201));
Assert::IsTrue(stopped.bubbleStopped);
Assert::IsFalse(stopped.frameworkDefaultPrevented);
Assert::AreEqual(0, parentCalls);
leafRaw->setRouteCallback([](StellarX::RoutedKeyEvent&)
{ return StellarX::KeyRouteResult{ false, true }; });
const auto prevented = RouteKeyboard(
window, MakeKeyInput(202));
Assert::IsFalse(prevented.bubbleStopped);
Assert::IsTrue(prevented.frameworkDefaultPrevented);
Assert::AreEqual(1, parentCalls);
}
TEST_METHOD(PendingRemovalSkipsLaterAncestorAndCommitsAfterEvent)
{
Window window(240, 160, 1);
int parentCalls = 0;
auto canvas = std::make_unique<KeyRouteTestCanvas>();
KeyRouteTestCanvas* canvasRaw = canvas.get();
const auto canvasHandle = canvas->getHandle();
auto leaf = std::make_unique<KeyRouteTestControl>();
KeyRouteTestControl* leafRaw = leaf.get();
const auto leafHandle = canvas->addControl(std::move(leaf));
canvasRaw->setRouteCallback([&](StellarX::RoutedKeyEvent&)
{
++parentCalls;
return StellarX::KeyRouteResult{};
});
leafRaw->setRouteCallback([&](StellarX::RoutedKeyEvent&)
{
Assert::IsTrue(window.removeControl(canvasHandle));
Assert::IsNotNull(window.findControl(canvasHandle));
return StellarX::KeyRouteResult{};
});
window.addControl(std::move(canvas));
window.getFocusManager().requestFocus({ leafHandle, 0 });
const auto outcome = RouteKeyboard(
window, MakeKeyInput(301));
Assert::AreEqual<size_t>(1, outcome.deliveredTargets);
Assert::AreEqual<size_t>(1, outcome.skippedTargets);
Assert::AreEqual(0, parentCalls);
Assert::IsNull(window.findControl(canvasHandle));
}
TEST_METHOD(NestedRoutePreservesOuterManagedDispatchAndMutationBoundary)
{
Window window(300, 180, 1);
auto canvas = std::make_unique<KeyRouteTestCanvas>();
auto leaf = std::make_unique<KeyRouteTestControl>();
KeyRouteTestControl* leafRaw = leaf.get();
const auto leafHandle = canvas->addControl(std::move(leaf));
window.addControl(std::move(canvas));
auto sibling = std::make_unique<FakeControl>();
const auto siblingHandle = window.addControl(std::move(sibling));
window.getFocusManager().requestFocus({ leafHandle, 0 });
int callbackDepth = 0;
leafRaw->setRouteCallback([&](StellarX::RoutedKeyEvent& event)
{
Assert::IsTrue(window.isManagedDispatchActive());
++callbackDepth;
if (event.sequence == 401)
{
Assert::IsTrue(window.removeControl(siblingHandle));
Assert::IsNotNull(window.findControl(siblingHandle));
const auto nested = RouteKeyboard(
window, MakeKeyInput(402));
Assert::AreEqual<size_t>(2, nested.deliveredTargets);
Assert::IsTrue(window.isManagedDispatchActive());
Assert::IsNotNull(window.findControl(siblingHandle));
}
return StellarX::KeyRouteResult{};
});
const auto outer = RouteKeyboard(
window, MakeKeyInput(401));
Assert::AreEqual<size_t>(2, outer.deliveredTargets);
Assert::AreEqual(2, callbackDepth);
Assert::IsFalse(window.isManagedDispatchActive());
Assert::IsNull(window.findControl(siblingHandle));
}
TEST_METHOD(FocusChangeAffectsNextEventButNotCurrentOriginalPath)
{
Window window(320, 180, 1);
int oldParentCalls = 0;
int replacementCalls = 0;
auto canvas = std::make_unique<KeyRouteTestCanvas>();
KeyRouteTestCanvas* canvasRaw = canvas.get();
auto leaf = std::make_unique<KeyRouteTestControl>();
KeyRouteTestControl* leafRaw = leaf.get();
const auto leafHandle = canvas->addControl(std::move(leaf));
canvasRaw->setRouteCallback([&](StellarX::RoutedKeyEvent&)
{
++oldParentCalls;
return StellarX::KeyRouteResult{};
});
window.addControl(std::move(canvas));
auto replacement = std::make_unique<KeyRouteTestControl>();
KeyRouteTestControl* replacementRaw = replacement.get();
const auto replacementHandle = window.addControl(std::move(replacement));
replacementRaw->setRouteCallback([&](StellarX::RoutedKeyEvent&)
{
++replacementCalls;
return StellarX::KeyRouteResult{};
});
leafRaw->setRouteCallback([&](StellarX::RoutedKeyEvent&)
{
window.getFocusManager().requestFocus({ replacementHandle, 0 });
return StellarX::KeyRouteResult{};
});
window.getFocusManager().requestFocus({ leafHandle, 0 });
const auto first = RouteKeyboard(
window, MakeKeyInput(501));
Assert::AreEqual<size_t>(2, first.deliveredTargets);
Assert::AreEqual(1, oldParentCalls);
Assert::AreEqual(0, replacementCalls);
const auto second = RouteKeyboard(
window, MakeKeyInput(502));
Assert::AreEqual(replacementHandle.value, second.originalTarget.owner.value);
Assert::AreEqual<size_t>(1, second.deliveredTargets);
Assert::AreEqual(1, replacementCalls);
}
TEST_METHOD(ActivationImeSystemAndModalGatesSuppressBusinessDelivery)
{
Window window(220, 140, 1);
int routeCalls = 0;
auto control = std::make_unique<KeyRouteTestControl>();
KeyRouteTestControl* raw = control.get();
const auto handle = window.addControl(std::move(control));
raw->setRouteCallback([&](StellarX::RoutedKeyEvent&)
{
++routeCalls;
return StellarX::KeyRouteResult{};
});
window.getFocusManager().requestFocus({ handle, 0 });
auto inactive = MakeKeyInput(551);
inactive.activationAtCapture = { false, 2 };
auto inactiveEnvironment = ActiveKeyboardEnvironment(2);
inactiveEnvironment.currentActivation.keyboardFocusActive = false;
const auto inactiveResult = RouteKeyboard(window, inactive, inactiveEnvironment);
Assert::IsTrue(inactiveResult.suppressedByInactiveWindow);
auto stale = MakeKeyInput(552);
stale.activationAtCapture = { true, 2 };
const auto staleResult = RouteKeyboard(
window, stale, ActiveKeyboardEnvironment(4));
Assert::IsTrue(staleResult.suppressedByStaleActivation);
auto processKey = MakeKeyInput(553, VK_PROCESSKEY);
const auto processResult = RouteKeyboard(window, processKey);
Assert::IsTrue(processResult.suppressedByIme);
auto composing = MakeKeyInput(554, 'A');
std::get<StellarX::KeyEvent>(composing.payload).imeComposingAtCapture = true;
const auto composingResult = RouteKeyboard(window, composing);
Assert::IsTrue(composingResult.suppressedByIme);
auto systemKey = MakeKeyInput(555, VK_MENU);
std::get<StellarX::KeyEvent>(systemKey.payload).systemKey = true;
const auto systemResult = RouteKeyboard(window, systemKey);
Assert::IsTrue(systemResult.suppressedBySystemKey);
auto modalEnvironment = ActiveKeyboardEnvironment();
modalEnvironment.modalBusinessSuppressed = true;
const auto modalResult = RouteKeyboard(
window, MakeKeyInput(556), modalEnvironment);
Assert::IsTrue(modalResult.suppressedByModal);
Assert::AreEqual(0, routeCalls);
const auto eligible = RouteKeyboard(window, MakeKeyInput(557));
Assert::AreEqual<size_t>(1, eligible.deliveredTargets);
Assert::AreEqual(1, routeCalls);
}
TEST_METHOD(DeactivationSuspendsFocusPathWithoutClearingLogicalFocus)
{
Window window(240, 160, 1);
auto canvas = std::make_unique<KeyRouteTestCanvas>();
KeyRouteTestCanvas* canvasRaw = canvas.get();
auto leaf = std::make_unique<KeyRouteTestControl>();
KeyRouteTestControl* leafRaw = leaf.get();
const auto leafHandle = canvas->addControl(std::move(leaf));
window.addControl(std::move(canvas));
window.getFocusManager().requestFocus({ leafHandle, 0 });
StellarX::NativeInputEvent activation;
activation.sequence = 561;
activation.activationAtCapture = { false, 2 };
StellarX::WindowActivationEvent payload;
payload.keyboardFocusActive = false;
activation.payload = payload;
auto environment = ActiveKeyboardEnvironment(2);
environment.currentActivation.keyboardFocusActive = false;
const auto outcome = RouteKeyboard(window, activation, environment);
Assert::IsTrue(outcome.activationEvent);
Assert::IsTrue(outcome.pathComplete);
Assert::IsTrue(outcome.focusedPathSuspended);
Assert::AreEqual<size_t>(2, outcome.deliveredTargets);
Assert::AreEqual(1, leafRaw->keyboardSuspensions);
Assert::AreEqual(1, canvasRaw->keyboardSuspensions);
Assert::IsTrue(window.getFocusManager().focusedTarget() ==
StellarX::FocusTargetId{ leafHandle, 0 });
const auto modalEntry =
StellarX::Internal::KeyboardRouter::suspendFocusedPath(window);
Assert::AreEqual<size_t>(2, modalEntry.deliveredTargets);
Assert::AreEqual(2, leafRaw->keyboardSuspensions);
Assert::AreEqual(2, canvasRaw->keyboardSuspensions);
}
TEST_METHOD(TabTraversalUsesCurrentOrderAndWrapsInBothDirections)
{
Window window(240, 160, 1);
auto middle = std::make_unique<FocusTestControl>(20);
const auto middleHandle = window.addControl(std::move(middle));
auto first = std::make_unique<FocusTestControl>(10);
const auto firstHandle = window.addControl(std::move(first));
auto last = std::make_unique<FocusTestControl>(30);
const auto lastHandle = window.addControl(std::move(last));
const auto fromEmpty = RouteKeyboard(window, MakeKeyInput(571, VK_TAB));
Assert::IsTrue(fromEmpty.tabTraversalAttempted);
Assert::IsTrue(fromEmpty.tabFocusChanged);
Assert::IsTrue(fromEmpty.tabFocusedTarget ==
StellarX::FocusTargetId{ firstHandle, 0 });
const auto forward = RouteKeyboard(window, MakeKeyInput(572, VK_TAB));
Assert::IsTrue(forward.tabTraversalAttempted);
Assert::IsTrue(window.getFocusManager().focusedTarget() ==
StellarX::FocusTargetId{ middleHandle, 0 });
auto reverseInput = MakeKeyInput(573, VK_TAB);
std::get<StellarX::KeyEvent>(reverseInput.payload).modifiers.leftShift = true;
const auto reverse = RouteKeyboard(window, reverseInput);
Assert::IsTrue(reverse.tabTraversalAttempted);
Assert::IsTrue(window.getFocusManager().focusedTarget() ==
StellarX::FocusTargetId{ firstHandle, 0 });
const auto wrappedReverse = RouteKeyboard(window, reverseInput);
Assert::IsTrue(wrappedReverse.tabTraversalAttempted);
Assert::IsTrue(window.getFocusManager().focusedTarget() ==
StellarX::FocusTargetId{ lastHandle, 0 });
auto keyUp = MakeKeyInput(574, VK_TAB);
std::get<StellarX::KeyEvent>(keyUp.payload).action = StellarX::KeyAction::Up;
const auto keyUpResult = RouteKeyboard(window, keyUp);
Assert::IsFalse(keyUpResult.tabTraversalAttempted);
auto controlTab = MakeKeyInput(575, VK_TAB);
std::get<StellarX::KeyEvent>(controlTab.payload).modifiers.leftControl = true;
const auto controlTabResult = RouteKeyboard(window, controlTab);
Assert::IsFalse(controlTabResult.tabTraversalAttempted);
Assert::IsTrue(window.getFocusManager().focusedTarget() ==
StellarX::FocusTargetId{ lastHandle, 0 });
}
TEST_METHOD(TabDefaultPreventionIsIndependentFromStoppingBubble)
{
Window window(220, 140, 1);
auto first = std::make_unique<KeyRouteTestControl>();
KeyRouteTestControl* firstRaw = first.get();
const auto firstHandle = window.addControl(std::move(first));
auto second = std::make_unique<FocusTestControl>();
const auto secondHandle = window.addControl(std::move(second));
window.getFocusManager().requestFocus({ firstHandle, 0 });
firstRaw->setRouteCallback([](StellarX::RoutedKeyEvent&)
{ return StellarX::KeyRouteResult{ true, false }; });
const auto stoppedOnly = RouteKeyboard(window, MakeKeyInput(581, VK_TAB));
Assert::IsTrue(stoppedOnly.bubbleStopped);
Assert::IsFalse(stoppedOnly.frameworkDefaultPrevented);
Assert::IsTrue(stoppedOnly.tabTraversalAttempted);
Assert::IsTrue(window.getFocusManager().focusedTarget() ==
StellarX::FocusTargetId{ secondHandle, 0 });
window.getFocusManager().requestFocus({ firstHandle, 0 });
firstRaw->setRouteCallback([](StellarX::RoutedKeyEvent&)
{ return StellarX::KeyRouteResult{ false, true }; });
const auto prevented = RouteKeyboard(window, MakeKeyInput(582, VK_TAB));
Assert::IsFalse(prevented.bubbleStopped);
Assert::IsTrue(prevented.frameworkDefaultPrevented);
Assert::IsFalse(prevented.tabTraversalAttempted);
Assert::IsTrue(window.getFocusManager().focusedTarget() ==
StellarX::FocusTargetId{ firstHandle, 0 });
}
TEST_METHOD(TabRebuildsFocusTreeAfterRouteCallbacks)
{
Window window(240, 160, 1);
auto first = std::make_unique<KeyRouteTestControl>();
KeyRouteTestControl* firstRaw = first.get();
first->setTabIndex(0);
const auto firstHandle = window.addControl(std::move(first));
auto hiddenByCallback = std::make_unique<FocusTestControl>(1);
FocusTestControl* hiddenRaw = hiddenByCallback.get();
const auto hiddenHandle = window.addControl(std::move(hiddenByCallback));
auto last = std::make_unique<FocusTestControl>(2);
const auto lastHandle = window.addControl(std::move(last));
window.getFocusManager().requestFocus({ firstHandle, 0 });
firstRaw->setRouteCallback([&](StellarX::RoutedKeyEvent&)
{
hiddenRaw->setIsVisible(false);
return StellarX::KeyRouteResult{};
});
const auto afterHide = RouteKeyboard(window, MakeKeyInput(591, VK_TAB));
Assert::IsTrue(afterHide.tabTraversalAttempted);
Assert::IsTrue(window.getFocusManager().focusedTarget() ==
StellarX::FocusTargetId{ lastHandle, 0 });
hiddenRaw->setIsVisible(true);
auto reverseInput = MakeKeyInput(592, VK_TAB);
std::get<StellarX::KeyEvent>(reverseInput.payload).modifiers.rightShift = true;
const auto afterShow = RouteKeyboard(window, reverseInput);
Assert::IsTrue(afterShow.tabTraversalAttempted);
Assert::IsTrue(window.getFocusManager().focusedTarget() ==
StellarX::FocusTargetId{ hiddenHandle, 0 });
}
TEST_METHOD(EmptyTabResultIsNotCachedWhenStopMaterializesLater)
{
Window window(220, 140, 1);
auto canvas = std::make_unique<Canvas>(0, 0, 100, 100);
Canvas* canvasRaw = canvas.get();
window.addControl(std::move(canvas));
const auto empty = RouteKeyboard(window, MakeKeyInput(601, VK_TAB));
Assert::IsTrue(empty.tabTraversalAttempted);
Assert::IsFalse(empty.tabFocusChanged);
Assert::IsFalse(static_cast<bool>(window.getFocusManager().focusedTarget()));
auto materialized = std::make_unique<FocusTestControl>();
const auto materializedHandle = canvasRaw->addControl(std::move(materialized));
const auto afterMaterialize = RouteKeyboard(window, MakeKeyInput(602, VK_TAB));
Assert::IsTrue(afterMaterialize.tabTraversalAttempted);
Assert::IsTrue(afterMaterialize.tabFocusChanged);
Assert::IsTrue(window.getFocusManager().focusedTarget() ==
StellarX::FocusTargetId{ materializedHandle, 0 });
}
TEST_METHOD(TabTraversalStaysInsideActiveFocusScope)
{
Window window(260, 160, 1);
auto outside = std::make_unique<FocusTestControl>(0);
const auto outsideHandle = window.addControl(std::move(outside));
auto scopeRoot = std::make_unique<Canvas>(0, 0, 120, 100);
const auto scopeHandle = scopeRoot->getHandle();
auto first = std::make_unique<FocusTestControl>(1);
const auto firstHandle = scopeRoot->addControl(std::move(first));
auto second = std::make_unique<FocusTestControl>(2);
const auto secondHandle = scopeRoot->addControl(std::move(second));
window.addControl(std::move(scopeRoot));
const auto scope = window.getFocusManager().pushScope(
{ scopeHandle, 0 }, { firstHandle, 0 });
Assert::IsTrue(static_cast<bool>(scope));
const auto forward = RouteKeyboard(window, MakeKeyInput(611, VK_TAB));
Assert::IsTrue(forward.tabTraversalAttempted);
Assert::IsTrue(window.getFocusManager().focusedTarget() ==
StellarX::FocusTargetId{ secondHandle, 0 });
const auto wrapped = RouteKeyboard(window, MakeKeyInput(612, VK_TAB));
Assert::IsTrue(wrapped.tabTraversalAttempted);
Assert::IsTrue(window.getFocusManager().focusedTarget() ==
StellarX::FocusTargetId{ firstHandle, 0 });
Assert::IsFalse(window.getFocusManager().focusedTarget() ==
StellarX::FocusTargetId{ outsideHandle, 0 });
}
TEST_METHOD(ButtonSpaceArmsOnceAndActivatesOnKeyUp)
{
Window window(220, 140, 1);
int clicks = 0;
auto button = std::make_unique<Button>(10, 10, 90, 30, "space");
Button* raw = button.get();
const auto handle = window.addControl(std::move(button));
raw->setOnClickListener([&]() { ++clicks; });
window.getFocusManager().requestFocus({ handle, 0 });
const auto down = RouteKeyboard(window, MakeKeyInput(621, VK_SPACE));
Assert::IsTrue(down.frameworkDefaultPrevented);
Assert::IsFalse(down.bubbleStopped);
Assert::AreEqual(0, clicks);
Assert::IsFalse(raw->isClicked());
auto repeatInput = MakeKeyInput(622, VK_SPACE);
auto& repeatKey = std::get<StellarX::KeyEvent>(repeatInput.payload);
repeatKey.wasDown = true;
repeatKey.repeatCount = 2;
const auto repeat = RouteKeyboard(window, repeatInput);
Assert::IsTrue(repeat.frameworkDefaultPrevented);
Assert::AreEqual(0, clicks);
auto upInput = MakeKeyInput(623, VK_SPACE);
std::get<StellarX::KeyEvent>(upInput.payload).action = StellarX::KeyAction::Up;
const auto up = RouteKeyboard(window, upInput);
Assert::IsTrue(up.frameworkDefaultPrevented);
Assert::AreEqual(1, clicks);
Assert::IsFalse(raw->isClicked());
const auto duplicateUp = RouteKeyboard(window, upInput);
Assert::IsFalse(duplicateUp.frameworkDefaultPrevented);
Assert::AreEqual(1, clicks);
}
TEST_METHOD(ButtonEnterActivatesOnlyOnInitialKeyDown)
{
Window window(220, 140, 1);
int clicks = 0;
auto button = std::make_unique<Button>(10, 10, 90, 30, "enter");
Button* raw = button.get();
const auto handle = window.addControl(std::move(button));
raw->setOnClickListener([&]() { ++clicks; });
window.getFocusManager().requestFocus({ handle, 0 });
const auto first = RouteKeyboard(window, MakeKeyInput(631, VK_RETURN));
Assert::IsTrue(first.frameworkDefaultPrevented);
Assert::AreEqual(1, clicks);
auto repeatInput = MakeKeyInput(632, VK_RETURN);
std::get<StellarX::KeyEvent>(repeatInput.payload).wasDown = true;
const auto repeat = RouteKeyboard(window, repeatInput);
Assert::IsTrue(repeat.frameworkDefaultPrevented);
Assert::AreEqual(1, clicks);
auto upInput = MakeKeyInput(633, VK_RETURN);
std::get<StellarX::KeyEvent>(upInput.payload).action = StellarX::KeyAction::Up;
const auto up = RouteKeyboard(window, upInput);
Assert::IsFalse(up.frameworkDefaultPrevented);
Assert::AreEqual(1, clicks);
}
TEST_METHOD(ButtonSpaceArmCancelsOnEscapeFocusModeVisibilityAndSuspension)
{
Window window(260, 150, 1);
int clicks = 0;
auto button = std::make_unique<Button>(10, 10, 90, 30, "cancel");
Button* raw = button.get();
const auto handle = window.addControl(std::move(button));
raw->setOnClickListener([&]() { ++clicks; });
auto other = std::make_unique<FocusTestControl>();
const auto otherHandle = window.addControl(std::move(other));
window.getFocusManager().requestFocus({ handle, 0 });
auto arm = [&](std::uint64_t sequence)
{
const auto result = RouteKeyboard(window, MakeKeyInput(sequence, VK_SPACE));
Assert::IsTrue(result.frameworkDefaultPrevented);
};
auto release = [&](std::uint64_t sequence)
{
auto input = MakeKeyInput(sequence, VK_SPACE);
std::get<StellarX::KeyEvent>(input.payload).action = StellarX::KeyAction::Up;
RouteKeyboard(window, input);
};
arm(641);
const auto escape = RouteKeyboard(window, MakeKeyInput(642, VK_ESCAPE));
Assert::IsFalse(escape.frameworkDefaultPrevented);
release(643);
Assert::AreEqual(0, clicks);
arm(644);
window.getFocusManager().requestFocus({ otherHandle, 0 });
window.getFocusManager().requestFocus({ handle, 0 });
release(645);
Assert::AreEqual(0, clicks);
arm(646);
raw->setbuttonMode(StellarX::ButtonMode::DISABLED);
raw->setbuttonMode(StellarX::ButtonMode::NORMAL);
window.getFocusManager().requestFocus({ handle, 0 });
release(647);
Assert::AreEqual(0, clicks);
arm(648);
raw->setIsVisible(false);
raw->setIsVisible(true);
window.getFocusManager().requestFocus({ handle, 0 });
release(649);
Assert::AreEqual(0, clicks);
arm(650);
const auto suspended =
StellarX::Internal::KeyboardRouter::suspendFocusedPath(window);
Assert::IsTrue(suspended.focusedPathSuspended);
release(651);
Assert::AreEqual(0, clicks);
Assert::IsTrue(window.getFocusManager().focusedTarget() ==
StellarX::FocusTargetId{ handle, 0 });
}
TEST_METHOD(ButtonToggleStateRemainsIndependentFromKeyboardArm)
{
Window window(220, 140, 1);
int toggledOn = 0;
int toggledOff = 0;
auto button = std::make_unique<Button>(10, 10, 90, 30, "toggle",
StellarX::ButtonMode::TOGGLE);
Button* raw = button.get();
const auto handle = window.addControl(std::move(button));
raw->setOnToggleOnListener([&]() { ++toggledOn; });
raw->setOnToggleOffListener([&]() { ++toggledOff; });
window.getFocusManager().requestFocus({ handle, 0 });
RouteKeyboard(window, MakeKeyInput(661, VK_SPACE));
Assert::IsFalse(raw->isClicked());
RouteKeyboard(window, MakeKeyInput(662, VK_ESCAPE));
auto upInput = MakeKeyInput(663, VK_SPACE);
std::get<StellarX::KeyEvent>(upInput.payload).action = StellarX::KeyAction::Up;
RouteKeyboard(window, upInput);
Assert::IsFalse(raw->isClicked());
Assert::AreEqual(0, toggledOn);
Assert::AreEqual(0, toggledOff);
RouteKeyboard(window, MakeKeyInput(664, VK_SPACE));
RouteKeyboard(window, upInput);
Assert::IsTrue(raw->isClicked());
Assert::AreEqual(1, toggledOn);
RouteKeyboard(window, MakeKeyInput(665, VK_SPACE));
RouteKeyboard(window, MakeKeyInput(666, VK_ESCAPE));
RouteKeyboard(window, upInput);
Assert::IsTrue(raw->isClicked());
Assert::AreEqual(0, toggledOff);
RouteKeyboard(window, MakeKeyInput(667, VK_RETURN));
Assert::IsFalse(raw->isClicked());
Assert::AreEqual(1, toggledOff);
}
TEST_METHOD(ButtonActivationPreventsDefaultButStillBubblesToAncestors)
{
Window window(240, 160, 1);
int clicks = 0;
int ancestorCalls = 0;
auto canvas = std::make_unique<KeyRouteTestCanvas>();
KeyRouteTestCanvas* canvasRaw = canvas.get();
auto button = std::make_unique<Button>(10, 10, 90, 30, "bubble");
Button* buttonRaw = button.get();
const auto buttonHandle = canvas->addControl(std::move(button));
canvasRaw->setRouteCallback([&](StellarX::RoutedKeyEvent&)
{
++ancestorCalls;
return StellarX::KeyRouteResult{};
});
window.addControl(std::move(canvas));
buttonRaw->setOnClickListener([&]() { ++clicks; });
window.getFocusManager().requestFocus({ buttonHandle, 0 });
const auto down = RouteKeyboard(window, MakeKeyInput(671, VK_SPACE));
auto upInput = MakeKeyInput(672, VK_SPACE);
std::get<StellarX::KeyEvent>(upInput.payload).action = StellarX::KeyAction::Up;
const auto up = RouteKeyboard(window, upInput);
Assert::IsTrue(down.frameworkDefaultPrevented);
Assert::IsTrue(up.frameworkDefaultPrevented);
Assert::IsFalse(down.bubbleStopped);
Assert::IsFalse(up.bubbleStopped);
Assert::AreEqual<size_t>(2, down.deliveredTargets);
Assert::AreEqual<size_t>(2, up.deliveredTargets);
Assert::AreEqual(2, ancestorCalls);
Assert::AreEqual(1, clicks);
}
TEST_METHOD(NonKeyPayloadIsNotDeliveredByKeyboardRouter)
{
Window window(200, 120, 1);
int routeCalls = 0;
auto control = std::make_unique<KeyRouteTestControl>();
KeyRouteTestControl* raw = control.get();
const auto handle = window.addControl(std::move(control));
raw->setRouteCallback([&](StellarX::RoutedKeyEvent&)
{
++routeCalls;
return StellarX::KeyRouteResult{};
});
window.getFocusManager().requestFocus({ handle, 0 });
StellarX::NativeInputEvent native;
native.sequence = 601;
StellarX::TextInputEvent text;
text.text = L"x";
native.payload = std::move(text);
const auto outcome = RouteKeyboard(window, native);
Assert::IsFalse(outcome.keyEvent);
Assert::AreEqual<size_t>(0, outcome.deliveredTargets);
Assert::AreEqual(0, routeCalls);
}
};
// Stage 1 only validates capture-time snapshots, normalization, queue ordering,
// and fail-stop. No test in this class performs focus or control routing.
TEST_CLASS(NativeInputTests)
{
public:
TEST_METHOD(KeyLParamAndModifierSnapshotArePreserved)
{
StellarX::Internal::KeyModifierSnapshot modifiers;
modifiers.leftControl = true;
modifiers.rightAlt = true;
const LPARAM lParam = static_cast<LPARAM>(3u | (0x1Du << 16) |
(1u << 24) | (1u << 29) | (1u << 30));
const auto event = StellarX::Internal::makeKeyEvent(
WM_SYSKEYDOWN, VK_CONTROL, lParam, modifiers, true);
Assert::IsTrue(event.action == StellarX::KeyAction::Down);
Assert::AreEqual<UINT>(VK_CONTROL, event.virtualKey);
Assert::AreEqual<UINT>(VK_RCONTROL, event.physicalVirtualKey);
Assert::AreEqual<UINT>(0x1D, event.scanCode);
Assert::AreEqual<std::uint16_t>(3, event.repeatCount);
Assert::IsTrue(event.extended);
Assert::IsTrue(event.wasDown);
Assert::IsTrue(event.altContext);
Assert::IsTrue(event.systemKey);
Assert::IsTrue(event.imeComposingAtCapture);
Assert::IsTrue(event.modifiers.leftControl);
Assert::IsTrue(event.modifiers.rightAlt);
Assert::IsTrue(event.isAutoRepeat());
}
TEST_METHOD(ProcessKeyOwnsOnlyItsKeyAndDoesNotStartComposition)
{
StellarX::Internal::NativeInputContext context(nullptr);
context.collect(nullptr, WM_KEYDOWN, VK_PROCESSKEY, 1);
StellarX::NativeInputEvent event;
Assert::IsTrue(context.tryPopForTesting(event));
const auto* key = std::get_if<StellarX::KeyEvent>(&event.payload);
Assert::IsNotNull(key);
Assert::AreEqual<UINT>(VK_PROCESSKEY, key->virtualKey);
Assert::IsFalse(key->imeComposingAtCapture);
Assert::IsTrue(key->isImeOwnedAtCapture());
Assert::IsFalse(context.tryPopForTesting(event));
context.collect(nullptr, WM_KEYDOWN, 'A', 1);
Assert::IsTrue(context.tryPopForTesting(event));
key = std::get_if<StellarX::KeyEvent>(&event.payload);
Assert::IsNotNull(key);
Assert::IsFalse(key->imeComposingAtCapture);
Assert::IsFalse(key->isImeOwnedAtCapture());
}
TEST_METHOD(CompositionLifecycleIsCapturedIndependentlyFromKeyOwnership)
{
StellarX::Internal::NativeInputContext context(nullptr);
context.collect(nullptr, WM_IME_STARTCOMPOSITION, 0, 0);
context.collect(nullptr, WM_KEYDOWN, 'A', 1);
context.collect(nullptr, WM_IME_ENDCOMPOSITION, 0, 0);
StellarX::NativeInputEvent event;
Assert::IsTrue(context.tryPopForTesting(event));
const auto* start = std::get_if<StellarX::CompositionEvent>(&event.payload);
Assert::IsNotNull(start);
Assert::IsTrue(start->stage == StellarX::CompositionStage::Start);
Assert::AreNotEqual<std::uint64_t>(0, start->compositionId);
const std::uint64_t compositionId = start->compositionId;
Assert::IsTrue(context.tryPopForTesting(event));
const auto* key = std::get_if<StellarX::KeyEvent>(&event.payload);
Assert::IsNotNull(key);
Assert::IsTrue(key->imeComposingAtCapture);
Assert::IsFalse(key->isImeOwnedAtCapture());
Assert::IsTrue(context.tryPopForTesting(event));
const auto* cancel = std::get_if<StellarX::CompositionEvent>(&event.payload);
Assert::IsNotNull(cancel);
Assert::IsTrue(cancel->stage == StellarX::CompositionStage::Cancel);
Assert::AreEqual(compositionId, cancel->compositionId);
Assert::IsTrue(context.tryPopForTesting(event));
const auto* end = std::get_if<StellarX::CompositionEvent>(&event.payload);
Assert::IsNotNull(end);
Assert::IsTrue(end->stage == StellarX::CompositionStage::End);
Assert::AreEqual(compositionId, end->compositionId);
Assert::IsFalse(context.tryPopForTesting(event));
}
TEST_METHOD(ActivationGenerationIsPublishedBeforeEachCapturedEvent)
{
StellarX::Internal::NativeInputContext context(nullptr);
const auto initial = context.activationSnapshot();
Assert::IsFalse(initial.keyboardFocusActive);
Assert::AreNotEqual<std::uint64_t>(0, initial.generation);
context.collect(nullptr, WM_SETFOCUS, 0, 0);
context.collect(nullptr, WM_KEYDOWN, 'A', 1);
context.collect(nullptr, WM_KILLFOCUS, 0, 0);
context.collect(nullptr, WM_KEYDOWN, 'B', 1);
StellarX::NativeInputEvent event;
Assert::IsTrue(context.tryPopForTesting(event));
const auto* activated =
std::get_if<StellarX::WindowActivationEvent>(&event.payload);
Assert::IsNotNull(activated);
Assert::IsTrue(activated->keyboardFocusActive);
Assert::IsTrue(event.activationAtCapture.keyboardFocusActive);
const std::uint64_t activeGeneration = event.activationAtCapture.generation;
Assert::AreNotEqual(initial.generation, activeGeneration);
Assert::IsTrue(context.tryPopForTesting(event));
Assert::IsNotNull(std::get_if<StellarX::KeyEvent>(&event.payload));
Assert::IsTrue(event.activationAtCapture.keyboardFocusActive);
Assert::AreEqual(activeGeneration, event.activationAtCapture.generation);
Assert::IsTrue(context.tryPopForTesting(event));
const auto* deactivated =
std::get_if<StellarX::WindowActivationEvent>(&event.payload);
Assert::IsNotNull(deactivated);
Assert::IsFalse(deactivated->keyboardFocusActive);
Assert::IsFalse(event.activationAtCapture.keyboardFocusActive);
const std::uint64_t inactiveGeneration = event.activationAtCapture.generation;
Assert::AreNotEqual(activeGeneration, inactiveGeneration);
Assert::IsTrue(context.tryPopForTesting(event));
Assert::IsNotNull(std::get_if<StellarX::KeyEvent>(&event.payload));
Assert::IsFalse(event.activationAtCapture.keyboardFocusActive);
Assert::AreEqual(inactiveGeneration, event.activationAtCapture.generation);
Assert::IsFalse(context.tryPopForTesting(event));
const auto current = context.activationSnapshot();
Assert::IsFalse(current.keyboardFocusActive);
Assert::AreEqual(inactiveGeneration, current.generation);
}
TEST_METHOD(AnsiDbcsLeadAndTrailBecomeOneUtf16Event)
{
StellarX::Internal::NativeInputContext context(nullptr);
context.setAnsiCodePageForTesting(936);
context.collect(nullptr, WM_CHAR, 0xD6, 1);
StellarX::NativeInputEvent event;
Assert::IsFalse(context.tryPopForTesting(event));
context.collect(nullptr, WM_CHAR, 0xD0, 1);
Assert::IsTrue(context.tryPopForTesting(event));
const auto* text = std::get_if<StellarX::TextInputEvent>(&event.payload);
Assert::IsNotNull(text);
Assert::AreEqual<std::size_t>(1, text->text.size());
Assert::AreEqual<wchar_t>(static_cast<wchar_t>(0x4E2D), text->text[0]);
Assert::IsTrue(text->source == StellarX::TextSource::AnsiCharFallback);
Assert::AreEqual<std::uint64_t>(0, text->compositionId);
}
TEST_METHOD(UnicodeScalarProducesSurrogatePairAndConsumesMessage)
{
StellarX::Internal::NativeInputContext context(nullptr);
const auto query = context.collect(nullptr, WM_UNICHAR, UNICODE_NOCHAR, 0);
Assert::IsTrue(query.handledSynchronously);
Assert::AreEqual<LRESULT>(TRUE, query.result);
const auto actual = context.collect(nullptr, WM_UNICHAR, 0x1F600, 0);
Assert::IsTrue(actual.handledSynchronously);
Assert::AreEqual<LRESULT>(0, actual.result);
StellarX::NativeInputEvent event;
Assert::IsTrue(context.tryPopForTesting(event));
const auto* text = std::get_if<StellarX::TextInputEvent>(&event.payload);
Assert::IsNotNull(text);
Assert::AreEqual<std::size_t>(2, text->text.size());
Assert::AreEqual<wchar_t>(static_cast<wchar_t>(0xD83D), text->text[0]);
Assert::AreEqual<wchar_t>(static_cast<wchar_t>(0xDE00), text->text[1]);
Assert::IsTrue(text->source == StellarX::TextSource::UnicodeScalar);
}
TEST_METHOD(InvalidUnicodeScalarIsRejectedAndCounted)
{
StellarX::Internal::NativeInputContext context(nullptr);
const auto result = context.collect(nullptr, WM_UNICHAR, 0x110000, 0);
Assert::IsFalse(result.handledSynchronously);
StellarX::NativeInputEvent event;
Assert::IsFalse(context.tryPopForTesting(event));
Assert::AreEqual<std::uint64_t>(1,
context.normalizationStats().rejectedTextSequences);
}
TEST_METHOD(ImeResultEchoChannelsAreSuppressedWithoutContentWideDedup)
{
StellarX::Internal::NativeInputContext context(nullptr);
context.setAnsiCodePageForTesting(936);
const std::wstring committed(1, static_cast<wchar_t>(0x4E2D));
context.seedEchoForTesting(committed, 7);
context.collect(nullptr, WM_IME_CHAR, 0xD6D0, 0);
context.collect(nullptr, WM_CHAR, 0xD6, 0);
context.collect(nullptr, WM_CHAR, 0xD0, 0);
StellarX::NativeInputEvent event;
Assert::IsFalse(context.tryPopForTesting(event));
// A new physical key is an echo boundary. Equal later content is real input.
context.collect(nullptr, WM_KEYDOWN, 'A', 1);
context.collect(nullptr, WM_CHAR, 0xD6, 0);
context.collect(nullptr, WM_CHAR, 0xD0, 0);
Assert::IsTrue(context.tryPopForTesting(event));
Assert::IsNotNull(std::get_if<StellarX::KeyEvent>(&event.payload));
Assert::IsTrue(context.tryPopForTesting(event));
const auto* text = std::get_if<StellarX::TextInputEvent>(&event.payload);
Assert::IsNotNull(text);
Assert::AreEqual(committed, text->text);
Assert::IsFalse(context.tryPopForTesting(event));
}
TEST_METHOD(ImeEndDoesNotDiscardImmediatelyFollowingResultEcho)
{
StellarX::Internal::NativeInputContext context(nullptr);
context.setAnsiCodePageForTesting(936);
const std::wstring committed(1, static_cast<wchar_t>(0x4E2D));
context.seedEchoForTesting(committed, 9);
context.collect(nullptr, WM_IME_ENDCOMPOSITION, 0, 0);
context.collect(nullptr, WM_IME_CHAR, 0xD6D0, 0);
StellarX::NativeInputEvent event;
Assert::IsFalse(context.tryPopForTesting(event));
}
TEST_METHOD(UnrepresentableAnsiEchoDoesNotSuppressFollowingText)
{
StellarX::Internal::NativeInputContext context(nullptr);
context.setAnsiCodePageForTesting(936);
std::wstring committed;
committed.push_back(static_cast<wchar_t>(0xD83D));
committed.push_back(static_cast<wchar_t>(0xDE00));
BOOL usedDefault = FALSE;
const int required = WideCharToMultiByte(936, WC_NO_BEST_FIT_CHARS,
committed.data(), static_cast<int>(committed.size()), nullptr, 0,
nullptr, &usedDefault);
Assert::IsTrue(required > 0);
Assert::IsTrue(usedDefault != FALSE);
std::string replacement(static_cast<std::size_t>(required), '\0');
usedDefault = FALSE;
Assert::AreEqual(required, WideCharToMultiByte(936, WC_NO_BEST_FIT_CHARS,
committed.data(), static_cast<int>(committed.size()), replacement.data(),
required, nullptr, &usedDefault));
context.seedEchoForTesting(committed, 11);
for (const unsigned char byte : replacement)
context.collect(nullptr, WM_CHAR, byte, 0);
context.collect(nullptr, WM_CHAR, 'A', 0);
StellarX::NativeInputEvent event;
Assert::IsTrue(context.tryPopForTesting(event));
const auto* text = std::get_if<StellarX::TextInputEvent>(&event.payload);
Assert::IsNotNull(text);
Assert::AreEqual(std::wstring(L"A"), text->text);
Assert::IsFalse(context.tryPopForTesting(event));
}
TEST_METHOD(QueuePreservesStrictOrderAcrossProducerAndConsumerThreads)
{
constexpr std::uint64_t eventCount = 20000;
StellarX::Internal::NativeInputQueue queue;
std::atomic<bool> producerDone{ false };
std::atomic<bool> failed{ false };
std::thread producer([&]() {
for (std::uint64_t sequence = 1; sequence <= eventCount; ++sequence)
{
StellarX::NativeInputEvent event;
event.sequence = sequence;
event.payload = StellarX::KeyEvent{};
if (queue.push(std::move(event)) !=
StellarX::Internal::NativeInputQueuePushResult::Queued)
{
failed.store(true, std::memory_order_release);
break;
}
}
producerDone.store(true, std::memory_order_release);
});
std::uint64_t expected = 1;
while (!producerDone.load(std::memory_order_acquire) || expected <= eventCount)
{
StellarX::NativeInputEvent event;
const auto result = queue.tryPop(event);
if (result == StellarX::Internal::NativeInputQueuePopResult::Popped)
{
if (event.sequence != expected)
failed.store(true, std::memory_order_release);
++expected;
}
else if (result == StellarX::Internal::NativeInputQueuePopResult::UnexpectedFailure)
{
failed.store(true, std::memory_order_release);
break;
}
else
{
if (producerDone.load(std::memory_order_acquire) &&
(expected > eventCount || failed.load(std::memory_order_acquire)))
break;
std::this_thread::yield();
}
}
producer.join();
Assert::IsFalse(failed.load(std::memory_order_acquire));
Assert::AreEqual(eventCount + 1, expected);
const auto stats = queue.stats();
Assert::AreEqual(eventCount, stats.pushed);
Assert::AreEqual(eventCount, stats.popped);
Assert::AreEqual<std::size_t>(0, stats.currentDepth);
Assert::IsTrue(stats.maximumDepth > 0);
}
TEST_METHOD(AllocationFailureBecomesObservableFailStop)
{
StellarX::Internal::NativeInputContext context(nullptr);
context.forceNextQueuePushResultForTesting(
StellarX::Internal::NativeInputQueuePushResult::AllocationFailure);
context.collect(nullptr, WM_KEYDOWN, 'A', 1);
Assert::IsTrue(context.hasFault());
const auto fault = context.fault();
Assert::IsTrue(fault.code == StellarX::Internal::NativeInputFaultCode::OutOfMemory);
Assert::AreEqual<UINT>(WM_KEYDOWN, fault.message);
StellarX::NativeInputEvent event;
Assert::IsFalse(context.tryPopForTesting(event));
}
TEST_METHOD(ObservationDrainIsStrictAndLeavesNoBacklog)
{
StellarX::Internal::NativeInputContext context(nullptr);
context.collect(nullptr, WM_KEYDOWN, 'A', 1);
context.collect(nullptr, WM_KEYUP, 'A', static_cast<LPARAM>(1u << 31));
context.collect(nullptr, WM_CHAR, 'a', 1);
Assert::AreEqual<std::size_t>(3, context.drainObserved(16));
const auto observations = context.observationStats();
Assert::AreEqual<std::uint64_t>(3, observations.observed);
Assert::AreEqual<std::uint64_t>(2, observations.keyEvents);
Assert::AreEqual<std::uint64_t>(1, observations.textEvents);
Assert::IsTrue(observations.sequenceStrict);
Assert::AreEqual<std::size_t>(0, context.queueStats().currentDepth);
Assert::IsFalse(context.hasFault());
}
};
// 验证文件 sink 的原子切换、失败回退和轮换过程不会丢失日志行。
TEST_CLASS(LoggingTests)
{
public:
TEST_METHOD(SetConfigOpensAndDisablesFileAtomically)
{
// 无效的新文件配置不能覆盖当前可用配置,也不能关闭旧 sink。
const auto path = MakeTempLogPath("config");
std::filesystem::remove(path);
auto& logger = StellarX::SxLogger::Get();
StellarX::SxLogConfig config;
config.fileEnabled = true;
config.filePath = path.string();
config.fileAppend = false;
Assert::IsTrue(logger.setConfig(config));
logger.logLine(StellarX::SxLogLevel::Info, "test", __FILE__, __LINE__, __func__, "configured");
logger.disableFile();
Assert::IsTrue(std::filesystem::exists(path));
const auto before = logger.getConfigCopy();
config.fileEnabled = true;
config.filePath.clear();
Assert::IsFalse(logger.setConfig(config));
Assert::AreEqual(before.filePath, logger.getConfigCopy().filePath);
std::filesystem::remove(path);
}
TEST_METHOD(RotationPreservesEveryLine)
{
// 高频轮换可能发生在同一毫秒内,所有分片和当前文件合计必须保留每一行。
const auto path = MakeTempLogPath("rotate");
const auto directory = path.parent_path();
const auto prefix = path.filename().string() + ".";
std::filesystem::remove(path);
for (const auto& entry : std::filesystem::directory_iterator(directory))
if (entry.path().filename().string().rfind(prefix, 0) == 0) std::filesystem::remove(entry.path());
StellarX::FileSink sink;
sink.setRotateBytes(32);
Assert::IsTrue(sink.open(path.string(), false));
for (int i = 0; i < 30; ++i) sink.writeLine("line-" + std::to_string(i) + "\n");
sink.close();
int lineCount = 0;
for (const auto& entry : std::filesystem::directory_iterator(directory))
{
const auto name = entry.path().filename().string();
if (entry.path() != path && name.rfind(prefix, 0) != 0) continue;
{
std::ifstream input(entry.path());
std::string line;
while (std::getline(input, line)) ++lineCount;
}
std::filesystem::remove(entry.path());
}
Assert::AreEqual(30, lineCount);
}
};
}