完成 StellarX 核心重构、缺陷修复与测试收口
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
#include "StellarX.h"
|
||||
|
||||
#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";
|
||||
Window window(480, 320, 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 == "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();
|
||||
return window.runEventLoop();
|
||||
}
|
||||
Reference in New Issue
Block a user