#include #include #include #include #include #include #include #include namespace { using XCEngine::Input::KeyCode; using XCEngine::UI::UIPoint; using XCEngine::UI::UIRect; using XCEngine::UI::UIInputEvent; using XCEngine::UI::UIInputEventType; using XCEngine::UI::UIPointerButton; using XCEngine::UI::UIDrawCommand; using XCEngine::UI::UIDrawCommandType; using XCEngine::UI::UIDrawData; using XCEngine::UI::UIDrawList; using XCEngine::UI::Runtime::UIScreenAsset; using XCEngine::UI::Runtime::UIScreenFrameInput; using XCEngine::UI::Runtime::UIScreenPlayer; using XCEngine::UI::Runtime::UIDocumentScreenHost; namespace fs = std::filesystem; 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 = {}; }; UIScreenAsset BuildScreenAsset(const fs::path& viewPath, const char* screenId) { UIScreenAsset screen = {}; screen.screenId = screenId; screen.documentPath = viewPath.string(); return screen; } UIScreenFrameInput BuildInputState(std::uint64_t frameIndex) { UIScreenFrameInput input = {}; input.viewportRect = UIRect(0.0f, 0.0f, 960.0f, 640.0f); input.frameIndex = frameIndex; input.focused = true; return input; } std::string BuildTabStripMarkup() { return "\n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" "\n"; } bool DrawDataContainsText( const UIDrawData& drawData, const std::string& text) { for (const UIDrawList& drawList : drawData.GetDrawLists()) { for (const UIDrawCommand& command : drawList.GetCommands()) { if (command.type == UIDrawCommandType::Text && command.text == text) { return true; } } } return false; } const UIDrawCommand* FindTextCommand( const UIDrawData& drawData, const std::string& text) { for (const UIDrawList& drawList : drawData.GetDrawLists()) { for (const UIDrawCommand& command : drawList.GetCommands()) { if (command.type == UIDrawCommandType::Text && command.text == text) { return &command; } } } return nullptr; } UIInputEvent MakePointerButtonEvent( UIInputEventType type, const UIPoint& position) { UIInputEvent event = {}; event.type = type; event.pointerButton = UIPointerButton::Left; event.position = position; return event; } UIInputEvent MakeKeyDownEvent(KeyCode keyCode) { UIInputEvent event = {}; event.type = UIInputEventType::KeyDown; event.keyCode = static_cast(keyCode); return event; } } // namespace TEST(UIRuntimeTabStripValidationTest, EmptyTabStripProducesExplicitFrameError) { TempFileScope viewFile( "xcui_runtime_invalid_tab_strip", ".xcui", "\n" " \n" " \n" " \n" "\n"); UIDocumentScreenHost host = {}; UIScreenPlayer player(host); ASSERT_TRUE(player.Load(BuildScreenAsset(viewFile.Path(), "runtime.invalid_tab_strip"))); const auto& frame = player.Update(BuildInputState(1u)); EXPECT_NE(frame.errorMessage.find("broken-tabs"), std::string::npos); EXPECT_NE(frame.errorMessage.find("at least 1 Tab child"), std::string::npos); } TEST(UIRuntimeTabStripTest, PointerSelectingTabSwitchesVisibleContentAndPersists) { TempFileScope viewFile("xcui_runtime_tab_strip_pointer", ".xcui", BuildTabStripMarkup()); UIDocumentScreenHost host = {}; UIScreenPlayer player(host); ASSERT_TRUE(player.Load(BuildScreenAsset(viewFile.Path(), "runtime.tab_strip.pointer"))); const auto& initialFrame = player.Update(BuildInputState(1u)); EXPECT_TRUE(DrawDataContainsText(initialFrame.drawData, "Inspector Content")); EXPECT_FALSE(DrawDataContainsText(initialFrame.drawData, "Console Content")); const UIDrawCommand* consoleTab = FindTextCommand(initialFrame.drawData, "Console"); ASSERT_NE(consoleTab, nullptr); UIScreenFrameInput selectInput = BuildInputState(2u); selectInput.events.push_back(MakePointerButtonEvent(UIInputEventType::PointerButtonDown, consoleTab->position)); selectInput.events.push_back(MakePointerButtonEvent(UIInputEventType::PointerButtonUp, consoleTab->position)); const auto& selectedFrame = player.Update(selectInput); EXPECT_FALSE(DrawDataContainsText(selectedFrame.drawData, "Inspector Content")); EXPECT_TRUE(DrawDataContainsText(selectedFrame.drawData, "Console Content")); const auto& debugAfterSelect = host.GetInputDebugSnapshot(); EXPECT_EQ(debugAfterSelect.lastResult, "Tab selected"); EXPECT_NE(debugAfterSelect.focusedStateKey.find("/workspace-tabs/tab-console"), std::string::npos); const auto& persistedFrame = player.Update(BuildInputState(3u)); EXPECT_TRUE(DrawDataContainsText(persistedFrame.drawData, "Console Content")); EXPECT_FALSE(DrawDataContainsText(persistedFrame.drawData, "Inspector Content")); } TEST(UIRuntimeTabStripTest, KeyboardNavigationUpdatesSelectionAndFocus) { TempFileScope viewFile("xcui_runtime_tab_strip_keyboard", ".xcui", BuildTabStripMarkup()); UIDocumentScreenHost host = {}; UIScreenPlayer player(host); ASSERT_TRUE(player.Load(BuildScreenAsset(viewFile.Path(), "runtime.tab_strip.keyboard"))); const auto& initialFrame = player.Update(BuildInputState(1u)); const UIDrawCommand* inspectorTab = FindTextCommand(initialFrame.drawData, "Inspector"); ASSERT_NE(inspectorTab, nullptr); UIScreenFrameInput focusInput = BuildInputState(2u); focusInput.events.push_back(MakePointerButtonEvent(UIInputEventType::PointerButtonDown, inspectorTab->position)); focusInput.events.push_back(MakePointerButtonEvent(UIInputEventType::PointerButtonUp, inspectorTab->position)); player.Update(focusInput); UIScreenFrameInput rightInput = BuildInputState(3u); rightInput.events.push_back(MakeKeyDownEvent(KeyCode::Right)); const auto& consoleFrame = player.Update(rightInput); EXPECT_TRUE(DrawDataContainsText(consoleFrame.drawData, "Console Content")); EXPECT_FALSE(DrawDataContainsText(consoleFrame.drawData, "Inspector Content")); const auto& afterRight = host.GetInputDebugSnapshot(); EXPECT_EQ(afterRight.lastResult, "Tab navigated"); EXPECT_NE(afterRight.focusedStateKey.find("/workspace-tabs/tab-console"), std::string::npos); UIScreenFrameInput leftInput = BuildInputState(4u); leftInput.events.push_back(MakeKeyDownEvent(KeyCode::Left)); const auto& inspectorFrameAfterLeft = player.Update(leftInput); EXPECT_TRUE(DrawDataContainsText(inspectorFrameAfterLeft.drawData, "Inspector Content")); EXPECT_FALSE(DrawDataContainsText(inspectorFrameAfterLeft.drawData, "Console Content")); const auto& afterLeft = host.GetInputDebugSnapshot(); EXPECT_EQ(afterLeft.lastResult, "Tab navigated"); EXPECT_NE(afterLeft.focusedStateKey.find("/workspace-tabs/tab-inspector"), std::string::npos); UIScreenFrameInput endInput = BuildInputState(5u); endInput.events.push_back(MakeKeyDownEvent(KeyCode::End)); const auto& profilerFrame = player.Update(endInput); EXPECT_TRUE(DrawDataContainsText(profilerFrame.drawData, "Profiler Content")); EXPECT_FALSE(DrawDataContainsText(profilerFrame.drawData, "Inspector Content")); const auto& afterEnd = host.GetInputDebugSnapshot(); EXPECT_EQ(afterEnd.lastResult, "Tab navigated"); EXPECT_NE(afterEnd.focusedStateKey.find("/workspace-tabs/tab-profiler"), std::string::npos); UIScreenFrameInput homeInput = BuildInputState(6u); homeInput.events.push_back(MakeKeyDownEvent(KeyCode::Home)); const auto& inspectorFrame = player.Update(homeInput); EXPECT_TRUE(DrawDataContainsText(inspectorFrame.drawData, "Inspector Content")); EXPECT_FALSE(DrawDataContainsText(inspectorFrame.drawData, "Profiler Content")); const auto& afterHome = host.GetInputDebugSnapshot(); EXPECT_EQ(afterHome.lastResult, "Tab navigated"); EXPECT_NE(afterHome.focusedStateKey.find("/workspace-tabs/tab-inspector"), std::string::npos); }