Files
StellarX-kaifa/tests/UiFixture.cpp
T

231 lines
9.8 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "StellarX.h"
#include <array>
#include <filesystem>
#include <fstream>
#include <memory>
#include <string>
namespace
{
// 场景使用固定纯色,脚本只断言关键像素,不依赖字体抗锯齿或完整截图。
constexpr COLORREF Background = RGB(30, 50, 70);
constexpr COLORREF ButtonColor = RGB(80, 160, 220);
constexpr COLORREF ClickColor = RGB(220, 90, 70);
constexpr COLORREF HoverColor = RGB(250, 200, 80);
void WriteResult(const std::filesystem::path& resultDir, const std::string& scenario,
const std::string& payload)
{
// JSON 是脚本与 fixture 之间的稳定协议;先建目录,再一次性覆盖当前检查点。
std::filesystem::create_directories(resultDir);
std::ofstream output(resultDir / (scenario + ".json"), std::ios::trunc);
output << payload;
}
std::unique_ptr<Button> MakeButton(int x, int y, int w, int h, const std::string& text)
{
// 统一按钮颜色,使 UI 回归可以区分背景、悬停和点击后的绘制结果。
return std::make_unique<Button>(x, y, w, h, text, ClickColor, ButtonColor, HoverColor);
}
}
int main(int argc, char** argv)
{
// fixture 每次只运行一个确定性场景,并用唯一标题让驱动脚本精确绑定 HWND。
const std::string scenario = argc > 1 ? argv[1] : "geometry";
const std::filesystem::path resultDir = argc > 2 ? argv[2] : "TestResults";
const bool consumeProbeScenario = scenario == "input-consume-probe";
const int windowWidth = consumeProbeScenario ? 720 : 480;
const int windowHeight = consumeProbeScenario ? 420 : 320;
Window window(windowWidth, windowHeight, 1, Background, "StellarX.UI." + scenario);
if (scenario == "ownership")
{
// 点击后删除自身并新增状态标签,覆盖回调期间删除和新增的生命周期路径。
auto button = MakeButton(20, 20, 120, 40, "remove self");
Button* raw = button.get();
const auto handle = window.addControl(std::move(button));
raw->setOnClickListener([&]() {
window.removeControl(handle);
auto status = std::make_unique<Label>(20, 90, "removed safely", WHITE, Background);
status->setTextdisap(true);
window.addControl(std::move(status));
WriteResult(resultDir, scenario, "{\"removed\":true}");
});
}
else if (scenario == "geometry")
{
// 一次点击同时移动、缩放和隐藏多个控件,用于检查旧区域是否被完整恢复。
auto button = MakeButton(20, 20, 120, 40, "move");
Button* raw = button.get();
auto label = std::make_unique<Label>(20, 85, "hide label", WHITE, RGB(90, 130, 70));
Label* labelRaw = label.get();
auto textBox = std::make_unique<TextBox>(20, 120, 120, 36, "resize");
TextBox* textBoxRaw = textBox.get();
textBoxRaw->setTextBoxBk(RGB(230, 230, 210));
auto canvas = std::make_unique<Canvas>(20, 175, 120, 50);
Canvas* canvasRaw = canvas.get();
canvasRaw->setCanvasBkColor(RGB(40, 120, 90));
raw->setOnClickListener([&, raw, labelRaw, textBoxRaw, canvasRaw]() {
raw->setX(220);
labelRaw->setIsVisible(false);
textBoxRaw->setX(220);
textBoxRaw->setWidth(80);
canvasRaw->setX(220);
canvasRaw->setWidth(80);
WriteResult(resultDir, scenario,
"{\"moved\":true,\"x\":220,\"textWidth\":80,\"canvasWidth\":80,\"labelVisible\":false}");
});
window.addControl(std::move(button));
window.addControl(std::move(label));
window.addControl(std::move(textBox));
window.addControl(std::move(canvas));
}
else if (scenario == "tooltip")
{
// 场景不再发送第二次鼠标移动,专门验证 Window tick 能让静止提示到期显示。
auto button = MakeButton(20, 20, 120, 40, "tooltip");
button->enableTooltip(true);
button->setTooltipDelay(200);
button->setTooltipText("stationary tip");
window.addControl(std::move(button));
}
else if (scenario == "input-probe" || scenario == "input-consume-probe")
{
// 该场景只提供一个可聚焦的 EasyX 顶层窗口,不让控件消费或改写键盘/IME 消息。
const bool consumeProbe = scenario == "input-consume-probe";
const std::array<std::string, 5> instructions = consumeProbe
? std::array<std::string, 5>{
"StellarX stage 0B isolated consume probe",
"This window is driven by the bounded diagnostic runner.",
"Only one message, key and action rule is active.",
"No control receives business keyboard routing.",
"The runner will close this window after capture."
}
: std::array<std::string, 5>{
"StellarX input probe - manual IME run",
"1. Click this window, then switch to the requested IME.",
"2. Type only the agreed non-sensitive test strings.",
"3. Cover commit, candidate select, cancel and language switch.",
"4. Close the window after the checklist is complete."
};
int y = 30;
for (const std::string& line : instructions)
{
auto label = std::make_unique<Label>(20, y, line, WHITE, Background);
label->setTextdisap(true);
window.addControl(std::move(label));
y += 42;
}
if (consumeProbe)
{
// 与 runner 传给 IMM32 的固定客户区坐标完全一致:行区 (200,255)-(680,284),
// caret bottom 为 (225,282)。仅用于人工确认候选框没有落回桌面边缘。
auto anchorLabel = std::make_unique<Label>(20, 260, "IME anchor:", WHITE, Background);
anchorLabel->setTextdisap(true);
window.addControl(std::move(anchorLabel));
auto lineRegion = std::make_unique<Canvas>(200, 255, 480, 29);
lineRegion->setCanvasBkColor(RGB(45, 70, 95));
window.addControl(std::move(lineRegion));
auto caret = std::make_unique<Canvas>(224, 258, 2, 24);
caret->setCanvasBkColor(HoverColor);
window.addControl(std::move(caret));
}
}
else if (scenario == "native-close")
{
// 只创建顶层窗口,由 UI 驱动发送原生关闭命令并验证宿主循环自行退出。
}
else if (scenario == "modal-close")
{
// 窗口首次绘制后立即进入真正的模态循环,由 UI 驱动关闭宿主原生窗口。
// 该场景用于验证模态循环不会在 HWND 已销毁后继续轮询。
}
else if (scenario == "tab")
{
// 两个页签故意使用相同标题,验证索引身份而不是文本匹配决定激活状态。
auto tabs = std::make_unique<TabControl>(20, 20, 300, 180);
TabControl* raw = tabs.get();
raw->setActiveIndex(0);
auto firstButton = MakeButton(0, 0, 100, 30, "same");
Button* firstButtonRaw = firstButton.get();
auto secondButton = MakeButton(0, 0, 100, 30, "same");
Button* secondButtonRaw = secondButton.get();
auto firstPage = std::make_unique<Canvas>(0, 0, 300, 150);
Canvas* firstPageRaw = firstPage.get();
firstPageRaw->setCanvasBkColor(RGB(40, 100, 80));
auto secondPage = std::make_unique<Canvas>(0, 0, 300, 150);
Canvas* secondPageRaw = secondPage.get();
secondPageRaw->setCanvasBkColor(RGB(110, 70, 120));
raw->addTab(std::move(firstButton), std::move(firstPage));
raw->addTab(std::move(secondButton), std::move(secondPage));
window.addControl(std::move(tabs));
auto verify = MakeButton(340, 220, 110, 36, "verify");
verify->setOnClickListener([&, raw, firstButtonRaw, secondButtonRaw, firstPageRaw, secondPageRaw]() {
WriteResult(resultDir, scenario, std::string("{\"active\":") +
std::to_string(raw->getActiveIndex()) +
",\"firstSelected\":" + (firstButtonRaw->isClicked() ? "true" : "false") +
",\"secondSelected\":" + (secondButtonRaw->isClicked() ? "true" : "false") +
",\"firstPageVisible\":" + (firstPageRaw->IsVisible() ? "true" : "false") +
",\"secondPageVisible\":" + (secondPageRaw->IsVisible() ? "true" : "false") + "}");
});
window.addControl(std::move(verify));
}
else if (scenario == "table")
{
// 长表头和长单元格配合极窄宽度,验证最小宽度夹紧及 coverage 报告范围。
auto table = std::make_unique<Table>(20, 20);
Table* raw = table.get();
raw->setHeaders({ "very-long-header", "second-long-header", "third" });
raw->setData({ { "very-long-cell-value", "another-long-value", "value" } });
window.addControl(std::move(table));
auto resize = MakeButton(340, 250, 110, 36, "narrow");
resize->setOnClickListener([&, raw]() {
raw->setWidth(40);
const RECT coverage = raw->getManagedRepaintCoverageRect();
WriteResult(resultDir, scenario, std::string("{\"width\":") +
std::to_string(raw->getWidth()) +
",\"coverageLeft\":" + std::to_string(coverage.left) +
",\"coverageTop\":" + std::to_string(coverage.top) +
",\"coverageRight\":" + std::to_string(coverage.right) +
",\"coverageBottom\":" + std::to_string(coverage.bottom) + "}");
});
window.addControl(std::move(resize));
}
else if (scenario == "dialog")
{
// 先叠加两个非模态对话框,再只关闭上层,验证下层恢复和交互顺序。
struct DialogState { bool upperClosed = false; bool lowerClosed = false; };
auto state = std::make_shared<DialogState>();
auto writeDialogState = [&, state]() {
WriteResult(resultDir, scenario, std::string("{\"upperClosed\":") +
(state->upperClosed ? "true" : "false") + ",\"lowerClosed\":" +
(state->lowerClosed ? "true" : "false") + "}");
};
auto open = MakeButton(20, 20, 140, 40, "open dialogs");
open->setOnClickListener([&, state, writeDialogState]() {
StellarX::MessageBox::showAsync(window, "lower", "lower", StellarX::MessageBoxType::OK,
[state, writeDialogState](StellarX::MessageBoxResult) { state->lowerClosed = true; writeDialogState(); });
StellarX::MessageBox::showAsync(window, "upper", "upper", StellarX::MessageBoxType::OK,
[state, writeDialogState](StellarX::MessageBoxResult) { state->upperClosed = true; writeDialogState(); });
});
window.addControl(std::move(open));
}
else
{
// 未知场景按参数错误处理,避免脚本误把未覆盖场景当成成功。
WriteResult(resultDir, scenario, "{\"error\":\"unknown scenario\"}");
return 2;
}
window.draw();
if (scenario == "modal-close")
{
(void)StellarX::MessageBox::showModal(
window, "close the native host window", "modal close", StellarX::MessageBoxType::OK);
}
return window.runEventLoop();
}