2026-03-27 13:07:39 +08:00
|
|
|
#include <gtest/gtest.h>
|
|
|
|
|
|
|
|
|
|
#include <XCEngine/Scene/SceneRuntime.h>
|
|
|
|
|
#include <XCEngine/Scripting/IScriptRuntime.h>
|
|
|
|
|
#include <XCEngine/Scripting/ScriptComponent.h>
|
|
|
|
|
#include <XCEngine/Scripting/ScriptEngine.h>
|
2026-04-05 06:52:17 +08:00
|
|
|
#include <XCEngine/UI/Runtime/UIScreenStackController.h>
|
|
|
|
|
#include <XCEngine/UI/Runtime/UIScreenTypes.h>
|
|
|
|
|
#include <XCEngine/UI/Runtime/UISystem.h>
|
2026-03-27 13:07:39 +08:00
|
|
|
|
2026-04-05 06:52:17 +08:00
|
|
|
#include <chrono>
|
|
|
|
|
#include <filesystem>
|
|
|
|
|
#include <fstream>
|
2026-03-27 13:07:39 +08:00
|
|
|
#include <memory>
|
|
|
|
|
#include <string>
|
|
|
|
|
#include <vector>
|
|
|
|
|
|
|
|
|
|
using namespace XCEngine::Components;
|
|
|
|
|
using namespace XCEngine::Scripting;
|
2026-04-05 06:52:17 +08:00
|
|
|
using XCEngine::UI::Runtime::UIScreenAsset;
|
|
|
|
|
|
|
|
|
|
namespace fs = std::filesystem;
|
2026-03-27 13:07:39 +08:00
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
|
|
|
|
|
std::string LifecycleMethodToString(ScriptLifecycleMethod method) {
|
|
|
|
|
switch (method) {
|
|
|
|
|
case ScriptLifecycleMethod::Awake: return "Awake";
|
|
|
|
|
case ScriptLifecycleMethod::OnEnable: return "OnEnable";
|
|
|
|
|
case ScriptLifecycleMethod::Start: return "Start";
|
|
|
|
|
case ScriptLifecycleMethod::FixedUpdate: return "FixedUpdate";
|
|
|
|
|
case ScriptLifecycleMethod::Update: return "Update";
|
|
|
|
|
case ScriptLifecycleMethod::LateUpdate: return "LateUpdate";
|
|
|
|
|
case ScriptLifecycleMethod::OnDisable: return "OnDisable";
|
|
|
|
|
case ScriptLifecycleMethod::OnDestroy: return "OnDestroy";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return "Unknown";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class OrderedObserverComponent : public Component {
|
|
|
|
|
public:
|
|
|
|
|
explicit OrderedObserverComponent(std::vector<std::string>* events)
|
|
|
|
|
: m_events(events) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string GetName() const override { return "OrderedObserver"; }
|
|
|
|
|
|
|
|
|
|
void Start() override {
|
|
|
|
|
if (m_events) {
|
|
|
|
|
m_events->push_back("NativeStart:" + GetGameObject()->GetName());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void FixedUpdate() override {
|
|
|
|
|
if (m_events) {
|
|
|
|
|
m_events->push_back("NativeFixedUpdate:" + GetGameObject()->GetName());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Update(float deltaTime) override {
|
|
|
|
|
(void)deltaTime;
|
|
|
|
|
if (m_events) {
|
|
|
|
|
m_events->push_back("NativeUpdate:" + GetGameObject()->GetName());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void LateUpdate(float deltaTime) override {
|
|
|
|
|
(void)deltaTime;
|
|
|
|
|
if (m_events) {
|
|
|
|
|
m_events->push_back("NativeLateUpdate:" + GetGameObject()->GetName());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
std::vector<std::string>* m_events = nullptr;
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-05 06:52:17 +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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
~TempFileScope() {
|
|
|
|
|
std::error_code ec;
|
|
|
|
|
fs::remove(m_path, ec);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const fs::path& Path() const {
|
|
|
|
|
return m_path;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
fs::path m_path = {};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
std::string BuildViewMarkup(const char* heroTitle) {
|
|
|
|
|
return
|
|
|
|
|
"<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"
|
|
|
|
|
" </Column>\n"
|
|
|
|
|
"</View>\n";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
UIScreenAsset BuildScreenAsset(const fs::path& viewPath, const char* screenId) {
|
|
|
|
|
UIScreenAsset screen = {};
|
|
|
|
|
screen.screenId = screenId;
|
|
|
|
|
screen.documentPath = viewPath.string();
|
|
|
|
|
return screen;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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-08 16:09:15 +08:00
|
|
|
const XCEngine::UI::UIDrawCommand* FindFirstFilledRectCommand(
|
|
|
|
|
const XCEngine::UI::UIDrawData& drawData) {
|
|
|
|
|
for (const XCEngine::UI::UIDrawList& drawList : drawData.GetDrawLists()) {
|
|
|
|
|
for (const XCEngine::UI::UIDrawCommand& command : drawList.GetCommands()) {
|
|
|
|
|
if (command.type == XCEngine::UI::UIDrawCommandType::FilledRect) {
|
|
|
|
|
return &command;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 07:29:27 +08:00
|
|
|
const XCEngine::UI::Runtime::UISystemPresentedLayer* FindPresentedLayerById(
|
|
|
|
|
const XCEngine::UI::Runtime::UISystemFrameResult& frame,
|
|
|
|
|
XCEngine::UI::Runtime::UIScreenLayerId layerId) {
|
|
|
|
|
for (const XCEngine::UI::Runtime::UISystemPresentedLayer& layer : frame.layers) {
|
|
|
|
|
if (layer.layerId == layerId) {
|
|
|
|
|
return &layer;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-27 13:07:39 +08:00
|
|
|
class RecordingScriptRuntime : public IScriptRuntime {
|
|
|
|
|
public:
|
|
|
|
|
explicit RecordingScriptRuntime(std::vector<std::string>* events)
|
|
|
|
|
: m_events(events) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void OnRuntimeStart(Scene* scene) override {
|
|
|
|
|
if (m_events) {
|
|
|
|
|
m_events->push_back("RuntimeStart:" + (scene ? scene->GetName() : std::string("null")));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void OnRuntimeStop(Scene* scene) override {
|
|
|
|
|
if (m_events) {
|
|
|
|
|
m_events->push_back("RuntimeStop:" + (scene ? scene->GetName() : std::string("null")));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-03 13:22:30 +08:00
|
|
|
bool TryGetAvailableScriptClasses(
|
|
|
|
|
std::vector<ScriptClassDescriptor>& outClasses) const override {
|
|
|
|
|
outClasses.clear();
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-28 15:09:42 +08:00
|
|
|
bool TryGetClassFieldMetadata(
|
|
|
|
|
const std::string& assemblyName,
|
|
|
|
|
const std::string& namespaceName,
|
|
|
|
|
const std::string& className,
|
|
|
|
|
std::vector<ScriptFieldMetadata>& outFields) const override {
|
|
|
|
|
(void)assemblyName;
|
|
|
|
|
(void)namespaceName;
|
|
|
|
|
(void)className;
|
|
|
|
|
outFields.clear();
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-28 19:26:08 +08:00
|
|
|
bool TryGetClassFieldDefaultValues(
|
|
|
|
|
const std::string& assemblyName,
|
|
|
|
|
const std::string& namespaceName,
|
|
|
|
|
const std::string& className,
|
|
|
|
|
std::vector<ScriptFieldDefaultValue>& outFields) const override {
|
|
|
|
|
(void)assemblyName;
|
|
|
|
|
(void)namespaceName;
|
|
|
|
|
(void)className;
|
|
|
|
|
outFields.clear();
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-28 15:09:42 +08:00
|
|
|
bool TrySetManagedFieldValue(
|
|
|
|
|
const ScriptRuntimeContext& context,
|
|
|
|
|
const std::string& fieldName,
|
|
|
|
|
const ScriptFieldValue& value) override {
|
|
|
|
|
(void)context;
|
|
|
|
|
(void)fieldName;
|
|
|
|
|
(void)value;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool TryGetManagedFieldValue(
|
|
|
|
|
const ScriptRuntimeContext& context,
|
|
|
|
|
const std::string& fieldName,
|
|
|
|
|
ScriptFieldValue& outValue) const override {
|
|
|
|
|
(void)context;
|
|
|
|
|
(void)fieldName;
|
|
|
|
|
(void)outValue;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void SyncManagedFieldsToStorage(const ScriptRuntimeContext& context) override {
|
|
|
|
|
(void)context;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-27 13:07:39 +08:00
|
|
|
bool CreateScriptInstance(const ScriptRuntimeContext& context) override {
|
|
|
|
|
if (m_events) {
|
|
|
|
|
m_events->push_back("Create:" + Describe(context));
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void DestroyScriptInstance(const ScriptRuntimeContext& context) override {
|
|
|
|
|
if (m_events) {
|
|
|
|
|
m_events->push_back("Destroy:" + Describe(context));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void InvokeMethod(
|
|
|
|
|
const ScriptRuntimeContext& context,
|
|
|
|
|
ScriptLifecycleMethod method,
|
|
|
|
|
float deltaTime) override {
|
|
|
|
|
(void)deltaTime;
|
|
|
|
|
if (m_events) {
|
|
|
|
|
m_events->push_back(LifecycleMethodToString(method) + ":" + Describe(context));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
static std::string Describe(const ScriptRuntimeContext& context) {
|
|
|
|
|
const std::string gameObjectName = context.gameObject ? context.gameObject->GetName() : "null";
|
|
|
|
|
const std::string className = context.component ? context.component->GetFullClassName() : "null";
|
|
|
|
|
return gameObjectName + ":" + className;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::vector<std::string>* m_events = nullptr;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
class SceneRuntimeTest : public ::testing::Test {
|
|
|
|
|
protected:
|
|
|
|
|
void SetUp() override {
|
|
|
|
|
scriptEngine = &ScriptEngine::Get();
|
|
|
|
|
scriptEngine->OnRuntimeStop();
|
|
|
|
|
runtimeImpl = std::make_unique<RecordingScriptRuntime>(&events);
|
|
|
|
|
scriptEngine->SetRuntime(runtimeImpl.get());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TearDown() override {
|
|
|
|
|
runtime.Stop();
|
|
|
|
|
scriptEngine->OnRuntimeStop();
|
|
|
|
|
scriptEngine->SetRuntime(nullptr);
|
|
|
|
|
runtimeImpl.reset();
|
|
|
|
|
scene.reset();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Scene* CreateScene(const std::string& sceneName) {
|
|
|
|
|
scene = std::make_unique<Scene>(sceneName);
|
|
|
|
|
return scene.get();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ScriptComponent* AddScript(GameObject* gameObject, const std::string& namespaceName, const std::string& className) {
|
|
|
|
|
ScriptComponent* component = gameObject->AddComponent<ScriptComponent>();
|
|
|
|
|
component->SetScriptClass("GameScripts", namespaceName, className);
|
|
|
|
|
return component;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::vector<std::string> events;
|
|
|
|
|
std::unique_ptr<Scene> scene;
|
|
|
|
|
std::unique_ptr<RecordingScriptRuntime> runtimeImpl;
|
|
|
|
|
ScriptEngine* scriptEngine = nullptr;
|
|
|
|
|
SceneRuntime runtime;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
TEST_F(SceneRuntimeTest, StartAndStopForwardToScriptEngine) {
|
|
|
|
|
Scene* runtimeScene = CreateScene("RuntimeScene");
|
|
|
|
|
GameObject* host = runtimeScene->CreateGameObject("Host");
|
|
|
|
|
AddScript(host, "Gameplay", "Bootstrap");
|
|
|
|
|
|
|
|
|
|
runtime.Start(runtimeScene);
|
|
|
|
|
runtime.Stop();
|
|
|
|
|
|
|
|
|
|
const std::vector<std::string> expected = {
|
|
|
|
|
"RuntimeStart:RuntimeScene",
|
|
|
|
|
"Create:Host:Gameplay.Bootstrap",
|
|
|
|
|
"Awake:Host:Gameplay.Bootstrap",
|
|
|
|
|
"OnEnable:Host:Gameplay.Bootstrap",
|
|
|
|
|
"OnDisable:Host:Gameplay.Bootstrap",
|
|
|
|
|
"OnDestroy:Host:Gameplay.Bootstrap",
|
|
|
|
|
"Destroy:Host:Gameplay.Bootstrap",
|
|
|
|
|
"RuntimeStop:RuntimeScene"
|
|
|
|
|
};
|
|
|
|
|
EXPECT_EQ(events, expected);
|
|
|
|
|
EXPECT_FALSE(runtime.IsRunning());
|
|
|
|
|
EXPECT_EQ(runtime.GetScene(), nullptr);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST_F(SceneRuntimeTest, FrameOrderRunsScriptLifecycleBeforeNativeComponents) {
|
|
|
|
|
Scene* runtimeScene = CreateScene("RuntimeScene");
|
|
|
|
|
GameObject* host = runtimeScene->CreateGameObject("Host");
|
|
|
|
|
host->AddComponent<OrderedObserverComponent>(&events);
|
|
|
|
|
AddScript(host, "Gameplay", "Mover");
|
|
|
|
|
|
|
|
|
|
runtime.Start(runtimeScene);
|
|
|
|
|
events.clear();
|
|
|
|
|
|
|
|
|
|
runtime.FixedUpdate(0.02f);
|
|
|
|
|
runtime.Update(0.016f);
|
|
|
|
|
runtime.LateUpdate(0.016f);
|
|
|
|
|
|
|
|
|
|
const std::vector<std::string> expected = {
|
|
|
|
|
"FixedUpdate:Host:Gameplay.Mover",
|
|
|
|
|
"NativeFixedUpdate:Host",
|
|
|
|
|
"Start:Host:Gameplay.Mover",
|
|
|
|
|
"Update:Host:Gameplay.Mover",
|
|
|
|
|
"NativeStart:Host",
|
|
|
|
|
"NativeUpdate:Host",
|
|
|
|
|
"LateUpdate:Host:Gameplay.Mover",
|
|
|
|
|
"NativeLateUpdate:Host"
|
|
|
|
|
};
|
|
|
|
|
EXPECT_EQ(events, expected);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST_F(SceneRuntimeTest, InactiveSceneSkipsFrameExecution) {
|
|
|
|
|
Scene* runtimeScene = CreateScene("RuntimeScene");
|
|
|
|
|
GameObject* host = runtimeScene->CreateGameObject("Host");
|
|
|
|
|
host->AddComponent<OrderedObserverComponent>(&events);
|
|
|
|
|
AddScript(host, "Gameplay", "PausedScript");
|
|
|
|
|
runtimeScene->SetActive(false);
|
|
|
|
|
|
|
|
|
|
runtime.Start(runtimeScene);
|
|
|
|
|
events.clear();
|
|
|
|
|
|
|
|
|
|
runtime.FixedUpdate(0.02f);
|
|
|
|
|
runtime.Update(0.016f);
|
|
|
|
|
runtime.LateUpdate(0.016f);
|
|
|
|
|
|
|
|
|
|
EXPECT_TRUE(events.empty());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST_F(SceneRuntimeTest, StartingNewSceneStopsPreviousRuntimeFirst) {
|
|
|
|
|
Scene* firstScene = CreateScene("FirstScene");
|
|
|
|
|
GameObject* firstHost = firstScene->CreateGameObject("FirstHost");
|
|
|
|
|
AddScript(firstHost, "Gameplay", "FirstScript");
|
|
|
|
|
|
|
|
|
|
runtime.Start(firstScene);
|
|
|
|
|
|
|
|
|
|
std::unique_ptr<Scene> secondScene = std::make_unique<Scene>("SecondScene");
|
|
|
|
|
GameObject* secondHost = secondScene->CreateGameObject("SecondHost");
|
|
|
|
|
ScriptComponent* secondScript = AddScript(secondHost, "Gameplay", "SecondScript");
|
|
|
|
|
|
|
|
|
|
events.clear();
|
|
|
|
|
runtime.Start(secondScene.get());
|
|
|
|
|
|
|
|
|
|
const std::vector<std::string> expected = {
|
|
|
|
|
"OnDisable:FirstHost:Gameplay.FirstScript",
|
|
|
|
|
"OnDestroy:FirstHost:Gameplay.FirstScript",
|
|
|
|
|
"Destroy:FirstHost:Gameplay.FirstScript",
|
|
|
|
|
"RuntimeStop:FirstScene",
|
|
|
|
|
"RuntimeStart:SecondScene",
|
|
|
|
|
"Create:SecondHost:Gameplay.SecondScript",
|
|
|
|
|
"Awake:SecondHost:Gameplay.SecondScript",
|
|
|
|
|
"OnEnable:SecondHost:Gameplay.SecondScript"
|
|
|
|
|
};
|
|
|
|
|
EXPECT_EQ(events, expected);
|
|
|
|
|
EXPECT_EQ(runtime.GetScene(), secondScene.get());
|
|
|
|
|
EXPECT_TRUE(scriptEngine->HasRuntimeInstance(secondScript));
|
|
|
|
|
|
|
|
|
|
runtime.Stop();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 06:52:17 +08:00
|
|
|
TEST_F(SceneRuntimeTest, UpdateTicksUiRuntimeAndClearsQueuedInputEvents) {
|
|
|
|
|
Scene* runtimeScene = CreateScene("RuntimeScene");
|
|
|
|
|
runtime.Start(runtimeScene);
|
|
|
|
|
runtime.SetUIViewportRect(XCEngine::UI::UIRect(0.0f, 0.0f, 800.0f, 480.0f));
|
|
|
|
|
runtime.SetUIFocused(true);
|
|
|
|
|
|
|
|
|
|
TempFileScope menuView("xcui_scene_runtime_menu", ".xcui", BuildViewMarkup("Runtime Menu"));
|
|
|
|
|
const auto layerId = runtime.GetUIScreenStackController().PushMenu(
|
|
|
|
|
BuildScreenAsset(menuView.Path(), "runtime.menu"),
|
|
|
|
|
"menu");
|
|
|
|
|
ASSERT_NE(layerId, 0u);
|
|
|
|
|
|
|
|
|
|
XCEngine::UI::UIInputEvent textEvent = {};
|
|
|
|
|
textEvent.type = XCEngine::UI::UIInputEventType::Character;
|
|
|
|
|
textEvent.character = 'A';
|
|
|
|
|
runtime.QueueUIInputEvent(textEvent);
|
|
|
|
|
|
|
|
|
|
runtime.Update(0.016f);
|
|
|
|
|
|
|
|
|
|
const auto& firstFrame = runtime.GetLastUIFrame();
|
|
|
|
|
ASSERT_EQ(firstFrame.presentedLayerCount, 1u);
|
|
|
|
|
ASSERT_EQ(firstFrame.layers.size(), 1u);
|
|
|
|
|
EXPECT_EQ(firstFrame.frameIndex, 1u);
|
|
|
|
|
EXPECT_EQ(firstFrame.layers.front().stats.inputEventCount, 1u);
|
|
|
|
|
EXPECT_TRUE(DrawDataContainsText(firstFrame.drawData, "Runtime Menu"));
|
|
|
|
|
|
|
|
|
|
runtime.Update(0.016f);
|
|
|
|
|
|
|
|
|
|
const auto& secondFrame = runtime.GetLastUIFrame();
|
|
|
|
|
ASSERT_EQ(secondFrame.presentedLayerCount, 1u);
|
|
|
|
|
ASSERT_EQ(secondFrame.layers.size(), 1u);
|
|
|
|
|
EXPECT_EQ(secondFrame.frameIndex, 2u);
|
|
|
|
|
EXPECT_EQ(secondFrame.layers.front().stats.inputEventCount, 0u);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST_F(SceneRuntimeTest, StopClearsUiRuntimeState) {
|
|
|
|
|
Scene* runtimeScene = CreateScene("RuntimeScene");
|
|
|
|
|
runtime.Start(runtimeScene);
|
|
|
|
|
runtime.SetUIViewportRect(XCEngine::UI::UIRect(0.0f, 0.0f, 800.0f, 480.0f));
|
|
|
|
|
runtime.SetUIFocused(true);
|
|
|
|
|
|
|
|
|
|
TempFileScope menuView("xcui_scene_runtime_pause", ".xcui", BuildViewMarkup("Pause Menu"));
|
|
|
|
|
const auto layerId = runtime.GetUIScreenStackController().PushMenu(
|
|
|
|
|
BuildScreenAsset(menuView.Path(), "runtime.pause"),
|
|
|
|
|
"pause");
|
|
|
|
|
ASSERT_NE(layerId, 0u);
|
|
|
|
|
|
|
|
|
|
runtime.Update(0.016f);
|
|
|
|
|
ASSERT_EQ(runtime.GetUISystem().GetLayerCount(), 1u);
|
|
|
|
|
ASSERT_EQ(runtime.GetUIScreenStackController().GetEntryCount(), 1u);
|
|
|
|
|
ASSERT_EQ(runtime.GetLastUIFrame().presentedLayerCount, 1u);
|
|
|
|
|
|
|
|
|
|
runtime.Stop();
|
|
|
|
|
|
|
|
|
|
EXPECT_FALSE(runtime.IsRunning());
|
|
|
|
|
EXPECT_EQ(runtime.GetScene(), nullptr);
|
|
|
|
|
EXPECT_EQ(runtime.GetUISystem().GetLayerCount(), 0u);
|
|
|
|
|
EXPECT_EQ(runtime.GetUIScreenStackController().GetEntryCount(), 0u);
|
|
|
|
|
EXPECT_EQ(runtime.GetLastUIFrame().presentedLayerCount, 0u);
|
|
|
|
|
EXPECT_TRUE(runtime.GetLastUIFrame().layers.empty());
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-08 16:09:15 +08:00
|
|
|
TEST_F(SceneRuntimeTest, ClearQueuedUiInputEventsPreventsPendingDelivery) {
|
|
|
|
|
Scene* runtimeScene = CreateScene("RuntimeScene");
|
|
|
|
|
runtime.Start(runtimeScene);
|
|
|
|
|
runtime.SetUIViewportRect(XCEngine::UI::UIRect(0.0f, 0.0f, 800.0f, 480.0f));
|
|
|
|
|
runtime.SetUIFocused(true);
|
|
|
|
|
|
|
|
|
|
TempFileScope menuView("xcui_scene_runtime_clear_input", ".xcui", BuildViewMarkup("Clear Input Menu"));
|
|
|
|
|
const auto layerId = runtime.GetUIScreenStackController().PushMenu(
|
|
|
|
|
BuildScreenAsset(menuView.Path(), "runtime.clear.input"),
|
|
|
|
|
"clear-input");
|
|
|
|
|
ASSERT_NE(layerId, 0u);
|
|
|
|
|
|
|
|
|
|
XCEngine::UI::UIInputEvent textEvent = {};
|
|
|
|
|
textEvent.type = XCEngine::UI::UIInputEventType::Character;
|
|
|
|
|
textEvent.character = 'A';
|
|
|
|
|
runtime.QueueUIInputEvent(textEvent);
|
|
|
|
|
|
|
|
|
|
XCEngine::UI::UIInputEvent keyEvent = {};
|
|
|
|
|
keyEvent.type = XCEngine::UI::UIInputEventType::KeyDown;
|
|
|
|
|
keyEvent.keyCode = 13;
|
|
|
|
|
runtime.QueueUIInputEvent(keyEvent);
|
|
|
|
|
|
|
|
|
|
runtime.ClearQueuedUIInputEvents();
|
|
|
|
|
runtime.Update(0.016f);
|
|
|
|
|
|
|
|
|
|
const auto& clearedFrame = runtime.GetLastUIFrame();
|
|
|
|
|
ASSERT_EQ(clearedFrame.presentedLayerCount, 1u);
|
|
|
|
|
ASSERT_EQ(clearedFrame.layers.size(), 1u);
|
|
|
|
|
EXPECT_EQ(clearedFrame.frameIndex, 1u);
|
|
|
|
|
EXPECT_EQ(clearedFrame.layers.front().layerId, layerId);
|
|
|
|
|
EXPECT_EQ(clearedFrame.layers.front().stats.inputEventCount, 0u);
|
|
|
|
|
|
|
|
|
|
runtime.QueueUIInputEvent(textEvent);
|
|
|
|
|
runtime.Update(0.016f);
|
|
|
|
|
|
|
|
|
|
const auto& deliveredFrame = runtime.GetLastUIFrame();
|
|
|
|
|
ASSERT_EQ(deliveredFrame.presentedLayerCount, 1u);
|
|
|
|
|
ASSERT_EQ(deliveredFrame.layers.size(), 1u);
|
|
|
|
|
EXPECT_EQ(deliveredFrame.frameIndex, 2u);
|
|
|
|
|
EXPECT_EQ(deliveredFrame.layers.front().stats.inputEventCount, 1u);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST_F(SceneRuntimeTest, ViewportPersistsAcrossFramesAndResetsAfterStop) {
|
|
|
|
|
Scene* runtimeScene = CreateScene("RuntimeScene");
|
|
|
|
|
runtime.Start(runtimeScene);
|
|
|
|
|
runtime.SetUIViewportRect(XCEngine::UI::UIRect(32.0f, 48.0f, 900.0f, 500.0f));
|
|
|
|
|
runtime.SetUIFocused(true);
|
|
|
|
|
|
|
|
|
|
TempFileScope menuView("xcui_scene_runtime_viewport", ".xcui", BuildViewMarkup("Viewport Menu"));
|
|
|
|
|
const UIScreenAsset screenAsset = BuildScreenAsset(menuView.Path(), "runtime.viewport.menu");
|
|
|
|
|
ASSERT_NE(runtime.GetUIScreenStackController().PushMenu(screenAsset, "viewport-menu"), 0u);
|
|
|
|
|
|
|
|
|
|
runtime.Update(0.016f);
|
|
|
|
|
|
|
|
|
|
const auto& firstFrame = runtime.GetLastUIFrame();
|
|
|
|
|
const auto* firstBackground = FindFirstFilledRectCommand(firstFrame.drawData);
|
|
|
|
|
ASSERT_NE(firstBackground, nullptr);
|
|
|
|
|
EXPECT_EQ(firstFrame.frameIndex, 1u);
|
|
|
|
|
EXPECT_FLOAT_EQ(firstBackground->rect.x, 32.0f);
|
|
|
|
|
EXPECT_FLOAT_EQ(firstBackground->rect.y, 48.0f);
|
|
|
|
|
EXPECT_FLOAT_EQ(firstBackground->rect.width, 900.0f);
|
|
|
|
|
EXPECT_FLOAT_EQ(firstBackground->rect.height, 500.0f);
|
|
|
|
|
|
|
|
|
|
runtime.Update(0.016f);
|
|
|
|
|
|
|
|
|
|
const auto& secondFrame = runtime.GetLastUIFrame();
|
|
|
|
|
const auto* secondBackground = FindFirstFilledRectCommand(secondFrame.drawData);
|
|
|
|
|
ASSERT_NE(secondBackground, nullptr);
|
|
|
|
|
EXPECT_EQ(secondFrame.frameIndex, 2u);
|
|
|
|
|
EXPECT_FLOAT_EQ(secondBackground->rect.x, 32.0f);
|
|
|
|
|
EXPECT_FLOAT_EQ(secondBackground->rect.y, 48.0f);
|
|
|
|
|
EXPECT_FLOAT_EQ(secondBackground->rect.width, 900.0f);
|
|
|
|
|
EXPECT_FLOAT_EQ(secondBackground->rect.height, 500.0f);
|
|
|
|
|
|
|
|
|
|
runtime.Stop();
|
|
|
|
|
|
|
|
|
|
runtime.Start(runtimeScene);
|
|
|
|
|
runtime.SetUIFocused(true);
|
|
|
|
|
ASSERT_NE(runtime.GetUIScreenStackController().PushMenu(screenAsset, "viewport-menu-reset"), 0u);
|
|
|
|
|
runtime.Update(0.016f);
|
|
|
|
|
|
|
|
|
|
const auto& restartedFrame = runtime.GetLastUIFrame();
|
|
|
|
|
const auto* restartedBackground = FindFirstFilledRectCommand(restartedFrame.drawData);
|
|
|
|
|
ASSERT_NE(restartedBackground, nullptr);
|
|
|
|
|
EXPECT_EQ(restartedFrame.frameIndex, 1u);
|
|
|
|
|
EXPECT_FLOAT_EQ(restartedBackground->rect.x, 0.0f);
|
|
|
|
|
EXPECT_FLOAT_EQ(restartedBackground->rect.y, 0.0f);
|
|
|
|
|
EXPECT_FLOAT_EQ(restartedBackground->rect.width, 640.0f);
|
|
|
|
|
EXPECT_FLOAT_EQ(restartedBackground->rect.height, 360.0f);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 07:29:27 +08:00
|
|
|
TEST_F(SceneRuntimeTest, LayeredSceneUiRoutesInputOnlyToTopInteractivePresentedLayer) {
|
|
|
|
|
Scene* runtimeScene = CreateScene("RuntimeScene");
|
|
|
|
|
runtime.Start(runtimeScene);
|
|
|
|
|
runtime.SetUIViewportRect(XCEngine::UI::UIRect(0.0f, 0.0f, 1280.0f, 720.0f));
|
|
|
|
|
runtime.SetUIFocused(true);
|
|
|
|
|
|
|
|
|
|
TempFileScope gameplayView("xcui_scene_runtime_gameplay", ".xcui", BuildViewMarkup("Gameplay Layer"));
|
|
|
|
|
TempFileScope overlayView("xcui_scene_runtime_overlay", ".xcui", BuildViewMarkup("Overlay Layer"));
|
|
|
|
|
|
|
|
|
|
XCEngine::UI::Runtime::UIScreenLayerOptions gameplayOptions = {};
|
|
|
|
|
gameplayOptions.debugName = "gameplay";
|
|
|
|
|
gameplayOptions.acceptsInput = true;
|
|
|
|
|
gameplayOptions.blocksLayersBelow = false;
|
|
|
|
|
|
|
|
|
|
XCEngine::UI::Runtime::UIScreenLayerOptions overlayOptions = {};
|
|
|
|
|
overlayOptions.debugName = "overlay";
|
|
|
|
|
overlayOptions.acceptsInput = true;
|
|
|
|
|
overlayOptions.blocksLayersBelow = false;
|
|
|
|
|
|
|
|
|
|
const auto gameplayLayerId = runtime.GetUIScreenStackController().PushScreen(
|
|
|
|
|
BuildScreenAsset(gameplayView.Path(), "runtime.gameplay"),
|
|
|
|
|
gameplayOptions);
|
|
|
|
|
const auto overlayLayerId = runtime.GetUIScreenStackController().PushScreen(
|
|
|
|
|
BuildScreenAsset(overlayView.Path(), "runtime.overlay"),
|
|
|
|
|
overlayOptions);
|
|
|
|
|
ASSERT_NE(gameplayLayerId, 0u);
|
|
|
|
|
ASSERT_NE(overlayLayerId, 0u);
|
|
|
|
|
|
|
|
|
|
XCEngine::UI::UIInputEvent textEvent = {};
|
|
|
|
|
textEvent.type = XCEngine::UI::UIInputEventType::Character;
|
|
|
|
|
textEvent.character = 'I';
|
|
|
|
|
runtime.QueueUIInputEvent(textEvent);
|
|
|
|
|
|
|
|
|
|
runtime.Update(0.016f);
|
|
|
|
|
|
|
|
|
|
const auto& frame = runtime.GetLastUIFrame();
|
|
|
|
|
ASSERT_EQ(frame.presentedLayerCount, 2u);
|
|
|
|
|
ASSERT_EQ(frame.skippedLayerCount, 0u);
|
|
|
|
|
ASSERT_EQ(frame.layers.size(), 2u);
|
|
|
|
|
|
|
|
|
|
const auto* gameplayLayer = FindPresentedLayerById(frame, gameplayLayerId);
|
|
|
|
|
const auto* overlayLayer = FindPresentedLayerById(frame, overlayLayerId);
|
|
|
|
|
ASSERT_NE(gameplayLayer, nullptr);
|
|
|
|
|
ASSERT_NE(overlayLayer, nullptr);
|
|
|
|
|
EXPECT_EQ(gameplayLayer->stats.inputEventCount, 0u);
|
|
|
|
|
EXPECT_EQ(overlayLayer->stats.inputEventCount, 1u);
|
|
|
|
|
EXPECT_TRUE(DrawDataContainsText(frame.drawData, "Gameplay Layer"));
|
|
|
|
|
EXPECT_TRUE(DrawDataContainsText(frame.drawData, "Overlay Layer"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST_F(SceneRuntimeTest, BlockingLayerSkipsLowerLayersAndOwnsQueuedInput) {
|
|
|
|
|
Scene* runtimeScene = CreateScene("RuntimeScene");
|
|
|
|
|
runtime.Start(runtimeScene);
|
|
|
|
|
runtime.SetUIViewportRect(XCEngine::UI::UIRect(0.0f, 0.0f, 1280.0f, 720.0f));
|
|
|
|
|
runtime.SetUIFocused(true);
|
|
|
|
|
|
|
|
|
|
TempFileScope gameplayView("xcui_scene_runtime_blocked_gameplay", ".xcui", BuildViewMarkup("Blocked Gameplay"));
|
|
|
|
|
TempFileScope modalView("xcui_scene_runtime_modal", ".xcui", BuildViewMarkup("Pause Modal"));
|
|
|
|
|
|
|
|
|
|
XCEngine::UI::Runtime::UIScreenLayerOptions gameplayOptions = {};
|
|
|
|
|
gameplayOptions.debugName = "gameplay";
|
|
|
|
|
gameplayOptions.acceptsInput = true;
|
|
|
|
|
gameplayOptions.blocksLayersBelow = false;
|
|
|
|
|
|
|
|
|
|
const auto gameplayLayerId = runtime.GetUIScreenStackController().PushScreen(
|
|
|
|
|
BuildScreenAsset(gameplayView.Path(), "runtime.blocked.gameplay"),
|
|
|
|
|
gameplayOptions);
|
|
|
|
|
const auto modalLayerId = runtime.GetUIScreenStackController().PushModal(
|
|
|
|
|
BuildScreenAsset(modalView.Path(), "runtime.pause.modal"),
|
|
|
|
|
"pause-modal");
|
|
|
|
|
ASSERT_NE(gameplayLayerId, 0u);
|
|
|
|
|
ASSERT_NE(modalLayerId, 0u);
|
|
|
|
|
|
|
|
|
|
XCEngine::UI::UIInputEvent textEvent = {};
|
|
|
|
|
textEvent.type = XCEngine::UI::UIInputEventType::Character;
|
|
|
|
|
textEvent.character = 'P';
|
|
|
|
|
runtime.QueueUIInputEvent(textEvent);
|
|
|
|
|
|
|
|
|
|
runtime.Update(0.016f);
|
|
|
|
|
|
|
|
|
|
const auto& frame = runtime.GetLastUIFrame();
|
|
|
|
|
ASSERT_EQ(frame.presentedLayerCount, 1u);
|
|
|
|
|
ASSERT_EQ(frame.skippedLayerCount, 1u);
|
|
|
|
|
ASSERT_EQ(frame.layers.size(), 1u);
|
|
|
|
|
|
|
|
|
|
const auto* modalLayer = FindPresentedLayerById(frame, modalLayerId);
|
|
|
|
|
ASSERT_NE(modalLayer, nullptr);
|
|
|
|
|
EXPECT_EQ(modalLayer->stats.inputEventCount, 1u);
|
|
|
|
|
EXPECT_EQ(FindPresentedLayerById(frame, gameplayLayerId), nullptr);
|
|
|
|
|
EXPECT_TRUE(DrawDataContainsText(frame.drawData, "Pause Modal"));
|
|
|
|
|
EXPECT_FALSE(DrawDataContainsText(frame.drawData, "Blocked Gameplay"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST_F(SceneRuntimeTest, HiddenTopLayerDoesNotStealInputFromVisibleUnderlyingLayer) {
|
|
|
|
|
Scene* runtimeScene = CreateScene("RuntimeScene");
|
|
|
|
|
runtime.Start(runtimeScene);
|
|
|
|
|
runtime.SetUIViewportRect(XCEngine::UI::UIRect(0.0f, 0.0f, 1280.0f, 720.0f));
|
|
|
|
|
runtime.SetUIFocused(true);
|
|
|
|
|
|
|
|
|
|
TempFileScope gameplayView("xcui_scene_runtime_visible_gameplay", ".xcui", BuildViewMarkup("Visible Gameplay"));
|
|
|
|
|
TempFileScope hiddenOverlayView("xcui_scene_runtime_hidden_overlay", ".xcui", BuildViewMarkup("Hidden Overlay"));
|
|
|
|
|
|
|
|
|
|
XCEngine::UI::Runtime::UIScreenLayerOptions gameplayOptions = {};
|
|
|
|
|
gameplayOptions.debugName = "gameplay";
|
|
|
|
|
gameplayOptions.acceptsInput = true;
|
|
|
|
|
gameplayOptions.blocksLayersBelow = false;
|
|
|
|
|
|
|
|
|
|
XCEngine::UI::Runtime::UIScreenLayerOptions hiddenOverlayOptions = {};
|
|
|
|
|
hiddenOverlayOptions.debugName = "hidden-overlay";
|
|
|
|
|
hiddenOverlayOptions.visible = false;
|
|
|
|
|
hiddenOverlayOptions.acceptsInput = true;
|
|
|
|
|
hiddenOverlayOptions.blocksLayersBelow = false;
|
|
|
|
|
|
|
|
|
|
const auto gameplayLayerId = runtime.GetUIScreenStackController().PushScreen(
|
|
|
|
|
BuildScreenAsset(gameplayView.Path(), "runtime.visible.gameplay"),
|
|
|
|
|
gameplayOptions);
|
|
|
|
|
const auto hiddenOverlayLayerId = runtime.GetUIScreenStackController().PushScreen(
|
|
|
|
|
BuildScreenAsset(hiddenOverlayView.Path(), "runtime.hidden.overlay"),
|
|
|
|
|
hiddenOverlayOptions);
|
|
|
|
|
ASSERT_NE(gameplayLayerId, 0u);
|
|
|
|
|
ASSERT_NE(hiddenOverlayLayerId, 0u);
|
|
|
|
|
|
|
|
|
|
XCEngine::UI::UIInputEvent textEvent = {};
|
|
|
|
|
textEvent.type = XCEngine::UI::UIInputEventType::Character;
|
|
|
|
|
textEvent.character = 'W';
|
|
|
|
|
runtime.QueueUIInputEvent(textEvent);
|
|
|
|
|
|
|
|
|
|
runtime.Update(0.016f);
|
|
|
|
|
|
|
|
|
|
const auto& frame = runtime.GetLastUIFrame();
|
|
|
|
|
ASSERT_EQ(frame.presentedLayerCount, 1u);
|
|
|
|
|
ASSERT_EQ(frame.skippedLayerCount, 1u);
|
|
|
|
|
ASSERT_EQ(frame.layers.size(), 1u);
|
|
|
|
|
|
|
|
|
|
const auto* gameplayLayer = FindPresentedLayerById(frame, gameplayLayerId);
|
|
|
|
|
ASSERT_NE(gameplayLayer, nullptr);
|
|
|
|
|
EXPECT_EQ(gameplayLayer->stats.inputEventCount, 1u);
|
|
|
|
|
EXPECT_EQ(FindPresentedLayerById(frame, hiddenOverlayLayerId), nullptr);
|
|
|
|
|
EXPECT_TRUE(DrawDataContainsText(frame.drawData, "Visible Gameplay"));
|
|
|
|
|
EXPECT_FALSE(DrawDataContainsText(frame.drawData, "Hidden Overlay"));
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-27 13:07:39 +08:00
|
|
|
} // namespace
|