2026-04-05 05:14:16 +08:00
|
|
|
#include <gtest/gtest.h>
|
|
|
|
|
|
|
|
|
|
#include <XCEngine/UI/Runtime/UIScreenDocumentHost.h>
|
|
|
|
|
#include <XCEngine/UI/Runtime/UIScreenPlayer.h>
|
2026-04-05 06:05:54 +08:00
|
|
|
#include <XCEngine/UI/Runtime/UIScreenStackController.h>
|
2026-04-05 05:14:16 +08:00
|
|
|
#include <XCEngine/UI/Runtime/UISystem.h>
|
|
|
|
|
|
2026-04-05 05:44:07 +08:00
|
|
|
#include <chrono>
|
2026-04-05 05:14:16 +08:00
|
|
|
#include <filesystem>
|
|
|
|
|
#include <fstream>
|
2026-04-05 05:44:07 +08:00
|
|
|
#include <string>
|
2026-04-05 05:14:16 +08:00
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
|
|
|
|
|
using XCEngine::UI::Runtime::UIScreenAsset;
|
|
|
|
|
using XCEngine::UI::Runtime::UIScreenFrameInput;
|
|
|
|
|
using XCEngine::UI::Runtime::UIScreenPlayer;
|
|
|
|
|
using XCEngine::UI::Runtime::UIDocumentScreenHost;
|
2026-04-05 06:05:54 +08:00
|
|
|
using XCEngine::UI::Runtime::UIScreenStackController;
|
2026-04-05 05:14:16 +08:00
|
|
|
using XCEngine::UI::Runtime::UISystem;
|
|
|
|
|
|
2026-04-05 05:44:07 +08:00
|
|
|
namespace fs = std::filesystem;
|
2026-04-05 05:14:16 +08:00
|
|
|
|
2026-04-05 05:44:07 +08:00
|
|
|
class TempFileScope {
|
|
|
|
|
public:
|
|
|
|
|
TempFileScope(std::string stem, std::string extension, std::string contents) {
|
|
|
|
|
const auto uniqueId = std::to_string(
|
|
|
|
|
std::chrono::steady_clock::now().time_since_epoch().count());
|
|
|
|
|
m_path = fs::temp_directory_path() / (std::move(stem) + "_" + uniqueId + std::move(extension));
|
|
|
|
|
std::ofstream output(m_path, std::ios::binary | std::ios::trunc);
|
|
|
|
|
output << contents;
|
|
|
|
|
}
|
2026-04-05 05:14:16 +08:00
|
|
|
|
2026-04-05 05:44:07 +08:00
|
|
|
~TempFileScope() {
|
|
|
|
|
std::error_code ec;
|
|
|
|
|
fs::remove(m_path, ec);
|
|
|
|
|
}
|
2026-04-05 05:14:16 +08:00
|
|
|
|
2026-04-05 05:44:07 +08:00
|
|
|
const fs::path& Path() const {
|
|
|
|
|
return m_path;
|
2026-04-05 05:14:16 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-05 05:44:07 +08:00
|
|
|
private:
|
|
|
|
|
fs::path m_path = {};
|
|
|
|
|
};
|
2026-04-05 05:14:16 +08:00
|
|
|
|
2026-04-05 05:44:07 +08:00
|
|
|
std::string BuildViewMarkup(const char* heroTitle, const char* overlayText = nullptr) {
|
|
|
|
|
std::string markup =
|
|
|
|
|
"<View name=\"Runtime Screen\">\n"
|
|
|
|
|
" <Column id=\"root\" padding=\"18\" gap=\"10\">\n"
|
|
|
|
|
" <Card id=\"hero\" title=\"" + std::string(heroTitle) + "\" subtitle=\"Shared XCUI runtime layer\" />\n"
|
|
|
|
|
" <Text id=\"status\" text=\"Ready for play\" />\n"
|
|
|
|
|
" <Row id=\"actions\" gap=\"12\">\n"
|
|
|
|
|
" <Button id=\"start\" text=\"Start\" />\n"
|
|
|
|
|
" <Button id=\"options\" text=\"Options\" />\n"
|
|
|
|
|
" </Row>\n";
|
|
|
|
|
if (overlayText != nullptr) {
|
|
|
|
|
markup += " <Card id=\"overlay\" title=\"" + std::string(overlayText) + "\" tone=\"accent\" />\n";
|
2026-04-05 05:14:16 +08:00
|
|
|
}
|
2026-04-05 05:44:07 +08:00
|
|
|
markup +=
|
|
|
|
|
" </Column>\n"
|
|
|
|
|
"</View>\n";
|
|
|
|
|
return markup;
|
|
|
|
|
}
|
2026-04-05 05:14:16 +08:00
|
|
|
|
2026-04-05 05:44:07 +08:00
|
|
|
UIScreenAsset BuildScreenAsset(const fs::path& viewPath, const char* screenId) {
|
|
|
|
|
UIScreenAsset screen = {};
|
|
|
|
|
screen.screenId = screenId;
|
|
|
|
|
screen.documentPath = viewPath.string();
|
|
|
|
|
return screen;
|
|
|
|
|
}
|
2026-04-05 05:14:16 +08:00
|
|
|
|
2026-04-05 05:44:07 +08:00
|
|
|
bool DrawDataContainsText(
|
|
|
|
|
const XCEngine::UI::UIDrawData& drawData,
|
|
|
|
|
const std::string& text) {
|
|
|
|
|
for (const XCEngine::UI::UIDrawList& drawList : drawData.GetDrawLists()) {
|
|
|
|
|
for (const XCEngine::UI::UIDrawCommand& command : drawList.GetCommands()) {
|
|
|
|
|
if (command.type == XCEngine::UI::UIDrawCommandType::Text &&
|
|
|
|
|
command.text == text) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
2026-04-05 05:14:16 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-05 05:44:07 +08:00
|
|
|
UIScreenFrameInput BuildInputState(std::uint64_t frameIndex = 1u) {
|
2026-04-05 05:14:16 +08:00
|
|
|
UIScreenFrameInput input = {};
|
2026-04-05 05:44:07 +08:00
|
|
|
input.viewportRect = XCEngine::UI::UIRect(0.0f, 0.0f, 800.0f, 480.0f);
|
2026-04-05 05:14:16 +08:00
|
|
|
input.frameIndex = frameIndex;
|
|
|
|
|
input.focused = true;
|
|
|
|
|
return input;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 05:44:07 +08:00
|
|
|
} // namespace
|
2026-04-05 05:14:16 +08:00
|
|
|
|
2026-04-05 05:44:07 +08:00
|
|
|
TEST(UIRuntimeTest, ScreenPlayerBuildsDrawDataFromDocumentTree) {
|
|
|
|
|
TempFileScope viewFile("xcui_runtime_screen", ".xcui", BuildViewMarkup("Runtime HUD"));
|
|
|
|
|
UIDocumentScreenHost host = {};
|
2026-04-05 05:14:16 +08:00
|
|
|
UIScreenPlayer player(host);
|
|
|
|
|
|
2026-04-05 05:44:07 +08:00
|
|
|
ASSERT_TRUE(player.Load(BuildScreenAsset(viewFile.Path(), "runtime.main_menu")));
|
2026-04-05 05:14:16 +08:00
|
|
|
|
2026-04-05 05:44:07 +08:00
|
|
|
const auto& frame = player.Update(BuildInputState());
|
|
|
|
|
EXPECT_TRUE(frame.stats.documentLoaded);
|
|
|
|
|
EXPECT_EQ(frame.stats.nodeCount, 7u);
|
|
|
|
|
EXPECT_EQ(frame.stats.drawListCount, frame.drawData.GetDrawListCount());
|
|
|
|
|
EXPECT_EQ(frame.stats.commandCount, frame.drawData.GetTotalCommandCount());
|
|
|
|
|
EXPECT_GE(frame.stats.textCommandCount, 5u);
|
|
|
|
|
EXPECT_TRUE(DrawDataContainsText(frame.drawData, "Runtime HUD"));
|
|
|
|
|
EXPECT_TRUE(DrawDataContainsText(frame.drawData, "Ready for play"));
|
|
|
|
|
EXPECT_TRUE(DrawDataContainsText(frame.drawData, "Start"));
|
|
|
|
|
EXPECT_TRUE(DrawDataContainsText(frame.drawData, "Options"));
|
2026-04-05 05:14:16 +08:00
|
|
|
EXPECT_EQ(player.GetPresentedFrameCount(), 1u);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 05:44:07 +08:00
|
|
|
TEST(UIRuntimeTest, UISystemForwardsActiveScreenToPlayer) {
|
|
|
|
|
TempFileScope baseView("xcui_runtime_base", ".xcui", BuildViewMarkup("Base Screen"));
|
|
|
|
|
TempFileScope overlayView("xcui_runtime_overlay", ".xcui", BuildViewMarkup("Overlay Screen", "Modal Dialog"));
|
2026-04-05 05:14:16 +08:00
|
|
|
|
2026-04-05 05:44:07 +08:00
|
|
|
UIDocumentScreenHost host = {};
|
2026-04-05 05:14:16 +08:00
|
|
|
UISystem system(host);
|
|
|
|
|
|
2026-04-05 05:44:07 +08:00
|
|
|
const auto baseLayer = system.PushScreen(
|
|
|
|
|
BuildScreenAsset(baseView.Path(), "runtime.base"));
|
|
|
|
|
ASSERT_NE(baseLayer, 0u);
|
2026-04-05 05:14:16 +08:00
|
|
|
|
2026-04-05 05:44:07 +08:00
|
|
|
XCEngine::UI::Runtime::UIScreenLayerOptions overlayOptions = {};
|
|
|
|
|
overlayOptions.debugName = "overlay";
|
|
|
|
|
overlayOptions.blocksLayersBelow = true;
|
|
|
|
|
const auto overlayLayer = system.PushScreen(
|
|
|
|
|
BuildScreenAsset(overlayView.Path(), "runtime.overlay"),
|
|
|
|
|
overlayOptions);
|
|
|
|
|
ASSERT_NE(overlayLayer, 0u);
|
2026-04-05 05:14:16 +08:00
|
|
|
|
2026-04-05 05:44:07 +08:00
|
|
|
const auto& frame = system.Update(BuildInputState(3u));
|
2026-04-05 05:14:16 +08:00
|
|
|
EXPECT_EQ(frame.presentedLayerCount, 1u);
|
|
|
|
|
EXPECT_EQ(frame.skippedLayerCount, 1u);
|
2026-04-05 05:44:07 +08:00
|
|
|
EXPECT_EQ(frame.layers.size(), 1u);
|
|
|
|
|
EXPECT_EQ(frame.layers.front().layerId, overlayLayer);
|
|
|
|
|
EXPECT_TRUE(DrawDataContainsText(frame.drawData, "Overlay Screen"));
|
|
|
|
|
EXPECT_TRUE(DrawDataContainsText(frame.drawData, "Modal Dialog"));
|
|
|
|
|
EXPECT_FALSE(DrawDataContainsText(frame.drawData, "Base Screen"));
|
2026-04-05 05:14:16 +08:00
|
|
|
}
|
2026-04-05 06:05:54 +08:00
|
|
|
|
|
|
|
|
TEST(UIRuntimeTest, ScreenStackControllerAppliesHudAndMenuLayerPolicies) {
|
|
|
|
|
TempFileScope hudView("xcui_runtime_hud", ".xcui", BuildViewMarkup("HUD Screen"));
|
|
|
|
|
TempFileScope menuView("xcui_runtime_menu", ".xcui", BuildViewMarkup("Pause Menu", "Paused"));
|
|
|
|
|
|
|
|
|
|
UIDocumentScreenHost host = {};
|
|
|
|
|
UISystem system(host);
|
|
|
|
|
UIScreenStackController stack(system);
|
|
|
|
|
|
|
|
|
|
const auto hudLayer = stack.PushHud(BuildScreenAsset(hudView.Path(), "runtime.hud"), "hud");
|
|
|
|
|
const auto menuLayer = stack.PushMenu(BuildScreenAsset(menuView.Path(), "runtime.menu"), "menu");
|
|
|
|
|
ASSERT_NE(hudLayer, 0u);
|
|
|
|
|
ASSERT_NE(menuLayer, 0u);
|
|
|
|
|
|
|
|
|
|
ASSERT_EQ(stack.GetEntryCount(), 2u);
|
|
|
|
|
ASSERT_NE(stack.GetTop(), nullptr);
|
|
|
|
|
EXPECT_EQ(stack.GetTop()->layerId, menuLayer);
|
|
|
|
|
|
|
|
|
|
const auto* hudOptions = system.FindLayerOptions(hudLayer);
|
|
|
|
|
const auto* menuOptions = system.FindLayerOptions(menuLayer);
|
|
|
|
|
ASSERT_NE(hudOptions, nullptr);
|
|
|
|
|
ASSERT_NE(menuOptions, nullptr);
|
|
|
|
|
EXPECT_FALSE(hudOptions->acceptsInput);
|
|
|
|
|
EXPECT_FALSE(hudOptions->blocksLayersBelow);
|
|
|
|
|
EXPECT_TRUE(menuOptions->acceptsInput);
|
|
|
|
|
EXPECT_TRUE(menuOptions->blocksLayersBelow);
|
|
|
|
|
|
|
|
|
|
const auto& frame = system.Update(BuildInputState(4u));
|
|
|
|
|
EXPECT_EQ(frame.presentedLayerCount, 1u);
|
|
|
|
|
EXPECT_TRUE(DrawDataContainsText(frame.drawData, "Pause Menu"));
|
|
|
|
|
EXPECT_FALSE(DrawDataContainsText(frame.drawData, "HUD Screen"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST(UIRuntimeTest, ScreenStackControllerReplaceTopSwapsMenuContent) {
|
|
|
|
|
TempFileScope pauseView("xcui_runtime_pause", ".xcui", BuildViewMarkup("Pause Menu"));
|
|
|
|
|
TempFileScope settingsView("xcui_runtime_settings", ".xcui", BuildViewMarkup("Settings Menu"));
|
|
|
|
|
|
|
|
|
|
UIDocumentScreenHost host = {};
|
|
|
|
|
UISystem system(host);
|
|
|
|
|
UIScreenStackController stack(system);
|
|
|
|
|
|
|
|
|
|
const auto pauseLayer = stack.PushMenu(BuildScreenAsset(pauseView.Path(), "runtime.pause"), "pause");
|
|
|
|
|
ASSERT_NE(pauseLayer, 0u);
|
|
|
|
|
|
|
|
|
|
XCEngine::UI::Runtime::UIScreenLayerOptions replacementOptions = {};
|
|
|
|
|
replacementOptions.debugName = "settings";
|
|
|
|
|
replacementOptions.acceptsInput = true;
|
|
|
|
|
replacementOptions.blocksLayersBelow = true;
|
|
|
|
|
ASSERT_TRUE(stack.ReplaceTop(
|
|
|
|
|
BuildScreenAsset(settingsView.Path(), "runtime.settings"),
|
|
|
|
|
replacementOptions));
|
|
|
|
|
|
|
|
|
|
ASSERT_EQ(stack.GetEntryCount(), 1u);
|
|
|
|
|
ASSERT_NE(stack.GetTop(), nullptr);
|
|
|
|
|
EXPECT_EQ(stack.GetTop()->asset.screenId, "runtime.settings");
|
|
|
|
|
EXPECT_NE(stack.GetTop()->layerId, pauseLayer);
|
|
|
|
|
|
|
|
|
|
const auto& frame = system.Update(BuildInputState(5u));
|
|
|
|
|
EXPECT_EQ(frame.presentedLayerCount, 1u);
|
|
|
|
|
EXPECT_TRUE(DrawDataContainsText(frame.drawData, "Settings Menu"));
|
|
|
|
|
EXPECT_FALSE(DrawDataContainsText(frame.drawData, "Pause Menu"));
|
|
|
|
|
}
|