#include #include "XCUIBackend/XCUIDemoRuntime.h" #include #include #include #include #include #include #include #include namespace { namespace fs = std::filesystem; using XCEngine::UI::UIDrawCommand; using XCEngine::UI::UIDrawCommandType; using XCEngine::UI::UIInputEvent; using XCEngine::UI::UIInputEventType; XCEngine::Editor::XCUIBackend::XCUIDemoInputState BuildInputState( float width = 720.0f, float height = 420.0f) { XCEngine::Editor::XCUIBackend::XCUIDemoInputState input = {}; input.canvasRect = XCEngine::UI::UIRect(0.0f, 0.0f, width, height); input.pointerPosition = XCEngine::UI::UIPoint(width * 0.5f, height * 0.5f); input.pointerInside = true; return input; } UIInputEvent MakeCharacterEvent(std::uint32_t character) { UIInputEvent event = {}; event.type = UIInputEventType::Character; event.character = character; return event; } UIInputEvent MakeKeyDownEvent( XCEngine::Input::KeyCode keyCode, bool repeat = false, bool shift = false, bool control = false, bool alt = false, bool super = false) { UIInputEvent event = {}; event.type = UIInputEventType::KeyDown; event.keyCode = static_cast(keyCode); event.repeat = repeat; event.modifiers.shift = shift; event.modifiers.control = control; event.modifiers.alt = alt; event.modifiers.super = super; return event; } fs::path FindDemoResourcePath() { fs::path probe = fs::current_path(); for (int i = 0; i < 8; ++i) { const fs::path canonicalCandidate = probe / "Assets/XCUI/NewEditor/Demo/View.xcui"; if (fs::exists(canonicalCandidate)) { return canonicalCandidate; } const fs::path legacyCandidate = probe / "new_editor/resources/xcui_demo_view.xcui"; if (fs::exists(legacyCandidate)) { return legacyCandidate; } if (!probe.has_parent_path()) { break; } probe = probe.parent_path(); } return {}; } std::vector CollectTextCommands(const XCEngine::UI::UIDrawData& drawData) { std::vector textCommands = {}; for (const XCEngine::UI::UIDrawList& drawList : drawData.GetDrawLists()) { for (const UIDrawCommand& command : drawList.GetCommands()) { if (command.type == UIDrawCommandType::Text) { textCommands.push_back(&command); } } } return textCommands; } const UIDrawCommand* FindTextCommand( const XCEngine::UI::UIDrawData& drawData, const std::string& text) { for (const XCEngine::UI::UIDrawList& drawList : drawData.GetDrawLists()) { for (const UIDrawCommand& command : drawList.GetCommands()) { if (command.type == UIDrawCommandType::Text && command.text == text) { return &command; } } } return nullptr; } class FileTimestampRestoreScope { public: explicit FileTimestampRestoreScope(fs::path path) : m_path(std::move(path)) { if (!m_path.empty() && fs::exists(m_path)) { m_originalWriteTime = fs::last_write_time(m_path); std::ifstream input(m_path, std::ios::binary); std::ostringstream stream; stream << input.rdbuf(); m_originalContents = stream.str(); m_valid = true; } } ~FileTimestampRestoreScope() { if (m_valid) { std::ofstream output(m_path, std::ios::binary | std::ios::trunc); output << m_originalContents; output.close(); fs::last_write_time(m_path, m_originalWriteTime); } } private: fs::path m_path; fs::file_time_type m_originalWriteTime = {}; std::string m_originalContents = {}; bool m_valid = false; }; } // namespace TEST(NewEditorXCUIDemoRuntimeTest, UpdateProvidesDeterministicFrameContainer) { XCEngine::Editor::XCUIBackend::XCUIDemoRuntime runtime; const bool reloadSucceeded = runtime.ReloadDocuments(); const auto& firstFrame = runtime.Update(BuildInputState()); EXPECT_EQ(firstFrame.stats.documentsReady, reloadSucceeded); EXPECT_EQ(firstFrame.stats.drawListCount, firstFrame.drawData.GetDrawListCount()); EXPECT_EQ(firstFrame.stats.commandCount, firstFrame.drawData.GetTotalCommandCount()); const auto& secondFrame = runtime.Update(BuildInputState()); EXPECT_GE(secondFrame.stats.treeGeneration, firstFrame.stats.treeGeneration); EXPECT_EQ(secondFrame.stats.drawListCount, secondFrame.drawData.GetDrawListCount()); EXPECT_EQ(secondFrame.stats.commandCount, secondFrame.drawData.GetTotalCommandCount()); if (secondFrame.stats.documentsReady) { EXPECT_GT(secondFrame.stats.elementCount, 0u); EXPECT_GT(secondFrame.stats.drawListCount, 0u); EXPECT_GT(secondFrame.stats.commandCount, 0u); } else { EXPECT_FALSE(secondFrame.stats.statusMessage.empty()); } } TEST(NewEditorXCUIDemoRuntimeTest, RuntimeFrameEmitsTextCommandsWithResolvedFontSizes) { XCEngine::Editor::XCUIBackend::XCUIDemoRuntime runtime; ASSERT_TRUE(runtime.ReloadDocuments()); const auto& frame = runtime.Update(BuildInputState()); ASSERT_TRUE(frame.stats.documentsReady); const std::vector textCommands = CollectTextCommands(frame.drawData); ASSERT_FALSE(textCommands.empty()); for (const UIDrawCommand* command : textCommands) { ASSERT_NE(command, nullptr); EXPECT_FALSE(command->text.empty()); EXPECT_GT(command->fontSize, 0.0f); } const UIDrawCommand* titleCommand = FindTextCommand(frame.drawData, "New XCUI Shell"); ASSERT_NE(titleCommand, nullptr); EXPECT_FLOAT_EQ(titleCommand->fontSize, 18.0f); const UIDrawCommand* metricValueCommand = FindTextCommand(frame.drawData, "Driven by runtime"); ASSERT_NE(metricValueCommand, nullptr); EXPECT_FLOAT_EQ(metricValueCommand->fontSize, 18.0f); const UIDrawCommand* buttonLabelCommand = FindTextCommand(frame.drawData, "Toggle Accent"); ASSERT_NE(buttonLabelCommand, nullptr); EXPECT_FLOAT_EQ(buttonLabelCommand->fontSize, 14.0f); EXPECT_NE( FindTextCommand(frame.drawData, "Single-line input, Enter submits, 0 chars"), nullptr); EXPECT_NE( FindTextCommand(frame.drawData, "Multiline input, click caret, Tab indent, 1 lines"), nullptr); } TEST(NewEditorXCUIDemoRuntimeTest, InputStateTransitionsAreAcceptedAndFrameStillBuilds) { XCEngine::Editor::XCUIBackend::XCUIDemoRuntime runtime; runtime.ReloadDocuments(); XCEngine::Editor::XCUIBackend::XCUIDemoInputState frameInput = BuildInputState(); frameInput.pointerPressed = true; frameInput.pointerDown = true; const auto& pressedFrame = runtime.Update(frameInput); frameInput.pointerPressed = false; frameInput.pointerReleased = true; frameInput.pointerDown = false; frameInput.shortcutPressed = true; const auto& releasedFrame = runtime.Update(frameInput); EXPECT_GE(releasedFrame.stats.treeGeneration, pressedFrame.stats.treeGeneration); EXPECT_EQ(releasedFrame.stats.drawListCount, releasedFrame.drawData.GetDrawListCount()); EXPECT_EQ(releasedFrame.stats.commandCount, releasedFrame.drawData.GetTotalCommandCount()); if (releasedFrame.stats.documentsReady) { EXPECT_GT(releasedFrame.stats.elementCount, 0u); EXPECT_GE(releasedFrame.stats.dirtyRootCount, 0u); } } TEST(NewEditorXCUIDemoRuntimeTest, DrainPendingCommandIdsReturnsEmptyForFramesWithoutCommands) { XCEngine::Editor::XCUIBackend::XCUIDemoRuntime runtime; ASSERT_TRUE(runtime.ReloadDocuments()); const auto& frame = runtime.Update(BuildInputState()); ASSERT_TRUE(frame.stats.documentsReady); const std::vector drainedCommandIds = runtime.DrainPendingCommandIds(); EXPECT_TRUE(drainedCommandIds.empty()); EXPECT_TRUE(runtime.DrainPendingCommandIds().empty()); } TEST(NewEditorXCUIDemoRuntimeTest, DrainPendingCommandIdsCapturesPointerActivationCommands) { XCEngine::Editor::XCUIBackend::XCUIDemoRuntime runtime; ASSERT_TRUE(runtime.ReloadDocuments()); const auto& baselineFrame = runtime.Update(BuildInputState()); ASSERT_TRUE(baselineFrame.stats.documentsReady); EXPECT_TRUE(runtime.DrainPendingCommandIds().empty()); XCEngine::UI::UIRect buttonRect = {}; ASSERT_TRUE(runtime.TryGetElementRect("toggleAccent", buttonRect)); const XCEngine::UI::UIPoint buttonCenter( buttonRect.x + buttonRect.width * 0.5f, buttonRect.y + buttonRect.height * 0.5f); XCEngine::Editor::XCUIBackend::XCUIDemoInputState pressedInput = BuildInputState(); pressedInput.pointerPosition = buttonCenter; pressedInput.pointerPressed = true; pressedInput.pointerDown = true; runtime.Update(pressedInput); EXPECT_TRUE(runtime.DrainPendingCommandIds().empty()); XCEngine::Editor::XCUIBackend::XCUIDemoInputState releasedInput = BuildInputState(); releasedInput.pointerPosition = buttonCenter; releasedInput.pointerReleased = true; const auto& toggledFrame = runtime.Update(releasedInput); ASSERT_TRUE(toggledFrame.stats.documentsReady); EXPECT_EQ(runtime.DrainPendingCommandIds(), std::vector({ "demo.toggleAccent" })); EXPECT_TRUE(runtime.DrainPendingCommandIds().empty()); } TEST(NewEditorXCUIDemoRuntimeTest, DrainPendingCommandIdsPreservesMultipleTextEditCommandsPerFrame) { XCEngine::Editor::XCUIBackend::XCUIDemoRuntime runtime; ASSERT_TRUE(runtime.ReloadDocuments()); const auto& baselineFrame = runtime.Update(BuildInputState()); ASSERT_TRUE(baselineFrame.stats.documentsReady); EXPECT_TRUE(runtime.DrainPendingCommandIds().empty()); XCEngine::UI::UIRect promptRect = {}; ASSERT_TRUE(runtime.TryGetElementRect("agentPrompt", promptRect)); const XCEngine::UI::UIPoint promptCenter( promptRect.x + promptRect.width * 0.5f, promptRect.y + promptRect.height * 0.5f); XCEngine::Editor::XCUIBackend::XCUIDemoInputState pressedInput = BuildInputState(); pressedInput.pointerPosition = promptCenter; pressedInput.pointerPressed = true; pressedInput.pointerDown = true; runtime.Update(pressedInput); EXPECT_TRUE(runtime.DrainPendingCommandIds().empty()); XCEngine::Editor::XCUIBackend::XCUIDemoInputState releasedInput = BuildInputState(); releasedInput.pointerPosition = promptCenter; releasedInput.pointerReleased = true; const auto& focusedFrame = runtime.Update(releasedInput); ASSERT_TRUE(focusedFrame.stats.documentsReady); EXPECT_EQ(focusedFrame.stats.focusedElementId, "agentPrompt"); EXPECT_EQ( runtime.DrainPendingCommandIds(), std::vector({ "demo.activate.agentPrompt" })); XCEngine::Editor::XCUIBackend::XCUIDemoInputState textInput = BuildInputState(); textInput.events.push_back(MakeCharacterEvent('A')); textInput.events.push_back(MakeCharacterEvent('I')); textInput.events.push_back(MakeKeyDownEvent(XCEngine::Input::KeyCode::Backspace)); const auto& typedFrame = runtime.Update(textInput); ASSERT_TRUE(typedFrame.stats.documentsReady); EXPECT_EQ(typedFrame.stats.focusedElementId, "agentPrompt"); EXPECT_EQ( runtime.DrainPendingCommandIds(), std::vector({ "demo.text.edit.agentPrompt", "demo.text.edit.agentPrompt", "demo.text.edit.agentPrompt" })); EXPECT_TRUE(runtime.DrainPendingCommandIds().empty()); } TEST(NewEditorXCUIDemoRuntimeTest, PointerToggleUpdatesFocusStatusTextAndAccentState) { XCEngine::Editor::XCUIBackend::XCUIDemoRuntime runtime; ASSERT_TRUE(runtime.ReloadDocuments()); const auto& baselineFrame = runtime.Update(BuildInputState()); ASSERT_TRUE(baselineFrame.stats.documentsReady); EXPECT_FALSE(baselineFrame.stats.accentEnabled); EXPECT_NE( FindTextCommand(baselineFrame.drawData, "Markup -> Layout -> Style -> DrawData"), nullptr); XCEngine::UI::UIRect buttonRect = {}; ASSERT_TRUE(runtime.TryGetElementRect("toggleAccent", buttonRect)); const XCEngine::UI::UIPoint buttonCenter( buttonRect.x + buttonRect.width * 0.5f, buttonRect.y + buttonRect.height * 0.5f); XCEngine::Editor::XCUIBackend::XCUIDemoInputState pressedInput = BuildInputState(); pressedInput.pointerPosition = buttonCenter; pressedInput.pointerPressed = true; pressedInput.pointerDown = true; const auto& pressedFrame = runtime.Update(pressedInput); ASSERT_TRUE(pressedFrame.stats.documentsReady); XCEngine::Editor::XCUIBackend::XCUIDemoInputState releasedInput = BuildInputState(); releasedInput.pointerPosition = buttonCenter; releasedInput.pointerReleased = true; const auto& toggledFrame = runtime.Update(releasedInput); ASSERT_TRUE(toggledFrame.stats.documentsReady); EXPECT_TRUE(toggledFrame.stats.accentEnabled); EXPECT_EQ(toggledFrame.stats.lastCommandId, "demo.toggleAccent"); EXPECT_EQ(toggledFrame.stats.focusedElementId, "toggleAccent"); const UIDrawCommand* focusStatusCommand = FindTextCommand( toggledFrame.drawData, "Focus: toggleAccent"); ASSERT_NE(focusStatusCommand, nullptr); EXPECT_FLOAT_EQ(focusStatusCommand->fontSize, 14.0f); } TEST(NewEditorXCUIDemoRuntimeTest, UpdateAutoReloadsWhenSourceTimestampChanges) { const fs::path viewPath = FindDemoResourcePath(); ASSERT_FALSE(viewPath.empty()); ASSERT_TRUE(fs::exists(viewPath)); FileTimestampRestoreScope restoreScope(viewPath); XCEngine::Editor::XCUIBackend::XCUIDemoRuntime runtime; ASSERT_TRUE(runtime.ReloadDocuments()); const auto& baselineFrame = runtime.Update(BuildInputState()); ASSERT_TRUE(baselineFrame.stats.documentsReady); XCEngine::UI::UIRect probeRect = {}; EXPECT_FALSE(runtime.TryGetElementRect("autoReloadProbe", probeRect)); std::ifstream input(viewPath, std::ios::binary); std::ostringstream stream; stream << input.rdbuf(); const std::string originalContents = stream.str(); input.close(); const std::string marker = "\n"; const std::size_t insertPosition = originalContents.rfind(marker); ASSERT_NE(insertPosition, std::string::npos); const std::string injectedNode = " \n"; std::string modifiedContents = originalContents; modifiedContents.insert(insertPosition, injectedNode); std::ofstream output(viewPath, std::ios::binary | std::ios::trunc); output << modifiedContents; output.close(); const fs::file_time_type originalWriteTime = fs::last_write_time(viewPath); fs::last_write_time(viewPath, originalWriteTime + std::chrono::seconds(2)); const auto& reloadedFrame = runtime.Update(BuildInputState()); EXPECT_TRUE(reloadedFrame.stats.documentsReady); EXPECT_GT(reloadedFrame.stats.elementCount, 0u); EXPECT_GT(reloadedFrame.stats.commandCount, 0u); EXPECT_TRUE(runtime.TryGetElementRect("autoReloadProbe", probeRect)); } TEST(NewEditorXCUIDemoRuntimeTest, TextFieldAcceptsUtf8CharactersAndBackspace) { XCEngine::Editor::XCUIBackend::XCUIDemoRuntime runtime; ASSERT_TRUE(runtime.ReloadDocuments()); const auto& baselineFrame = runtime.Update(BuildInputState()); ASSERT_TRUE(baselineFrame.stats.documentsReady); XCEngine::UI::UIRect promptRect = {}; ASSERT_TRUE(runtime.TryGetElementRect("agentPrompt", promptRect)); const XCEngine::UI::UIPoint promptCenter( promptRect.x + promptRect.width * 0.5f, promptRect.y + promptRect.height * 0.5f); XCEngine::Editor::XCUIBackend::XCUIDemoInputState pressedInput = BuildInputState(); pressedInput.pointerPosition = promptCenter; pressedInput.pointerPressed = true; pressedInput.pointerDown = true; runtime.Update(pressedInput); XCEngine::Editor::XCUIBackend::XCUIDemoInputState releasedInput = BuildInputState(); releasedInput.pointerPosition = promptCenter; releasedInput.pointerReleased = true; const auto& focusedFrame = runtime.Update(releasedInput); ASSERT_TRUE(focusedFrame.stats.documentsReady); EXPECT_EQ(focusedFrame.stats.focusedElementId, "agentPrompt"); XCEngine::Editor::XCUIBackend::XCUIDemoInputState textInput = BuildInputState(); textInput.events.push_back(MakeCharacterEvent('A')); textInput.events.push_back(MakeCharacterEvent('I')); textInput.events.push_back(MakeCharacterEvent(0x4F60u)); const auto& typedFrame = runtime.Update(textInput); ASSERT_TRUE(typedFrame.stats.documentsReady); EXPECT_EQ(typedFrame.stats.focusedElementId, "agentPrompt"); EXPECT_NE(FindTextCommand(typedFrame.drawData, "AIä½ "), nullptr); EXPECT_EQ(typedFrame.stats.lastCommandId, "demo.text.edit.agentPrompt"); XCEngine::Editor::XCUIBackend::XCUIDemoInputState backspaceInput = BuildInputState(); backspaceInput.events.push_back(MakeKeyDownEvent(XCEngine::Input::KeyCode::Backspace)); const auto& backspacedFrame = runtime.Update(backspaceInput); ASSERT_TRUE(backspacedFrame.stats.documentsReady); EXPECT_NE(FindTextCommand(backspacedFrame.drawData, "AI"), nullptr); EXPECT_EQ(backspacedFrame.stats.focusedElementId, "agentPrompt"); } TEST(NewEditorXCUIDemoRuntimeTest, TextAreaAcceptsMultilineInputAndCaretMovement) { XCEngine::Editor::XCUIBackend::XCUIDemoRuntime runtime; ASSERT_TRUE(runtime.ReloadDocuments()); const auto& baselineFrame = runtime.Update(BuildInputState()); ASSERT_TRUE(baselineFrame.stats.documentsReady); XCEngine::UI::UIRect notesRect = {}; ASSERT_TRUE(runtime.TryGetElementRect("sessionNotes", notesRect)); const XCEngine::UI::UIPoint notesCenter( notesRect.x + notesRect.width * 0.5f, notesRect.y + notesRect.height * 0.5f); XCEngine::Editor::XCUIBackend::XCUIDemoInputState pressedInput = BuildInputState(); pressedInput.pointerPosition = notesCenter; pressedInput.pointerPressed = true; pressedInput.pointerDown = true; runtime.Update(pressedInput); XCEngine::Editor::XCUIBackend::XCUIDemoInputState releasedInput = BuildInputState(); releasedInput.pointerPosition = notesCenter; releasedInput.pointerReleased = true; const auto& focusedFrame = runtime.Update(releasedInput); ASSERT_TRUE(focusedFrame.stats.documentsReady); EXPECT_EQ(focusedFrame.stats.focusedElementId, "sessionNotes"); XCEngine::Editor::XCUIBackend::XCUIDemoInputState textInput = BuildInputState(); textInput.events.push_back(MakeCharacterEvent('O')); textInput.events.push_back(MakeCharacterEvent('K')); textInput.events.push_back(MakeKeyDownEvent(XCEngine::Input::KeyCode::Enter)); textInput.events.push_back(MakeCharacterEvent('X')); const auto& typedFrame = runtime.Update(textInput); ASSERT_TRUE(typedFrame.stats.documentsReady); EXPECT_EQ(typedFrame.stats.focusedElementId, "sessionNotes"); EXPECT_EQ(typedFrame.stats.lastCommandId, "demo.text.edit.sessionNotes"); EXPECT_NE(FindTextCommand(typedFrame.drawData, "1"), nullptr); EXPECT_NE(FindTextCommand(typedFrame.drawData, "2"), nullptr); EXPECT_NE(FindTextCommand(typedFrame.drawData, "OK"), nullptr); EXPECT_NE(FindTextCommand(typedFrame.drawData, "X"), nullptr); EXPECT_NE( FindTextCommand(typedFrame.drawData, "Multiline input, click caret, Tab indent, 2 lines"), nullptr); const UIDrawCommand* secondLineText = FindTextCommand(typedFrame.drawData, "X"); ASSERT_NE(secondLineText, nullptr); XCEngine::Editor::XCUIBackend::XCUIDemoInputState secondLinePressed = BuildInputState(); secondLinePressed.pointerPosition = XCEngine::UI::UIPoint( secondLineText->position.x + 1.0f, secondLineText->position.y + secondLineText->fontSize * 0.5f); secondLinePressed.pointerPressed = true; secondLinePressed.pointerDown = true; runtime.Update(secondLinePressed); XCEngine::Editor::XCUIBackend::XCUIDemoInputState secondLineReleased = BuildInputState(); secondLineReleased.pointerPosition = secondLinePressed.pointerPosition; secondLineReleased.pointerReleased = true; const auto& secondLineCaretFrame = runtime.Update(secondLineReleased); ASSERT_TRUE(secondLineCaretFrame.stats.documentsReady); EXPECT_EQ(secondLineCaretFrame.stats.focusedElementId, "sessionNotes"); XCEngine::Editor::XCUIBackend::XCUIDemoInputState tabInput = BuildInputState(); tabInput.events.push_back(MakeKeyDownEvent(XCEngine::Input::KeyCode::Tab)); const auto& indentedFrame = runtime.Update(tabInput); ASSERT_TRUE(indentedFrame.stats.documentsReady); EXPECT_EQ(indentedFrame.stats.focusedElementId, "sessionNotes"); EXPECT_EQ(indentedFrame.stats.lastCommandId, "demo.text.edit.sessionNotes"); EXPECT_NE(FindTextCommand(indentedFrame.drawData, "OK"), nullptr); EXPECT_NE(FindTextCommand(indentedFrame.drawData, " X"), nullptr); const UIDrawCommand* firstLineText = FindTextCommand(indentedFrame.drawData, "OK"); ASSERT_NE(firstLineText, nullptr); XCEngine::Editor::XCUIBackend::XCUIDemoInputState firstLinePressed = BuildInputState(); firstLinePressed.pointerPosition = XCEngine::UI::UIPoint( firstLineText->position.x + 1.0f, firstLineText->position.y + firstLineText->fontSize * 0.5f); firstLinePressed.pointerPressed = true; firstLinePressed.pointerDown = true; runtime.Update(firstLinePressed); XCEngine::Editor::XCUIBackend::XCUIDemoInputState firstLineReleased = BuildInputState(); firstLineReleased.pointerPosition = firstLinePressed.pointerPosition; firstLineReleased.pointerReleased = true; const auto& caretFrame = runtime.Update(firstLineReleased); ASSERT_TRUE(caretFrame.stats.documentsReady); EXPECT_EQ(caretFrame.stats.focusedElementId, "sessionNotes"); XCEngine::Editor::XCUIBackend::XCUIDemoInputState editInput = BuildInputState(); editInput.events.push_back(MakeCharacterEvent('!')); const auto& editedFrame = runtime.Update(editInput); ASSERT_TRUE(editedFrame.stats.documentsReady); EXPECT_NE(FindTextCommand(editedFrame.drawData, "!OK"), nullptr); EXPECT_NE(FindTextCommand(editedFrame.drawData, " X"), nullptr); EXPECT_EQ(editedFrame.stats.focusedElementId, "sessionNotes"); }