#include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace XCEngine::Components; using namespace XCEngine::Scripting; using XCEngine::UI::Runtime::UIScreenAsset; namespace fs = std::filesystem; 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* 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* m_events = nullptr; }; 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 "\n" " \n" " \n" " \n" " \n" "\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; } 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; } 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; } class RecordingScriptRuntime : public IScriptRuntime { public: explicit RecordingScriptRuntime(std::vector* 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"))); } } bool TryGetAvailableScriptClasses( std::vector& outClasses) const override { outClasses.clear(); return false; } bool TryGetClassFieldMetadata( const std::string& assemblyName, const std::string& namespaceName, const std::string& className, std::vector& outFields) const override { (void)assemblyName; (void)namespaceName; (void)className; outFields.clear(); return false; } bool TryGetClassFieldDefaultValues( const std::string& assemblyName, const std::string& namespaceName, const std::string& className, std::vector& outFields) const override { (void)assemblyName; (void)namespaceName; (void)className; outFields.clear(); return false; } 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; } 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* m_events = nullptr; }; class SceneRuntimeTest : public ::testing::Test { protected: void SetUp() override { scriptEngine = &ScriptEngine::Get(); scriptEngine->OnRuntimeStop(); runtimeImpl = std::make_unique(&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(sceneName); return scene.get(); } ScriptComponent* AddScript(GameObject* gameObject, const std::string& namespaceName, const std::string& className) { ScriptComponent* component = gameObject->AddComponent(); component->SetScriptClass("GameScripts", namespaceName, className); return component; } std::vector events; std::unique_ptr scene; std::unique_ptr 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 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(&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 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(&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 secondScene = std::make_unique("SecondScene"); GameObject* secondHost = secondScene->CreateGameObject("SecondHost"); ScriptComponent* secondScript = AddScript(secondHost, "Gameplay", "SecondScript"); events.clear(); runtime.Start(secondScene.get()); const std::vector 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(); } 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()); } 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); } 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")); } } // namespace