2026-04-05 04:55:25 +08:00
|
|
|
#include <gtest/gtest.h>
|
|
|
|
|
|
|
|
|
|
#include "XCUIBackend/XCUIDemoRuntime.h"
|
|
|
|
|
|
|
|
|
|
#include <XCEngine/Input/InputTypes.h>
|
|
|
|
|
#include <XCEngine/UI/Types.h>
|
|
|
|
|
|
|
|
|
|
#include <chrono>
|
|
|
|
|
#include <filesystem>
|
|
|
|
|
#include <fstream>
|
|
|
|
|
#include <sstream>
|
|
|
|
|
#include <string>
|
|
|
|
|
#include <vector>
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 05:58:05 +08:00
|
|
|
UIInputEvent MakeKeyDownEvent(
|
|
|
|
|
XCEngine::Input::KeyCode keyCode,
|
|
|
|
|
bool repeat = false,
|
|
|
|
|
bool shift = false,
|
|
|
|
|
bool control = false,
|
|
|
|
|
bool alt = false,
|
|
|
|
|
bool super = false) {
|
2026-04-05 04:55:25 +08:00
|
|
|
UIInputEvent event = {};
|
|
|
|
|
event.type = UIInputEventType::KeyDown;
|
|
|
|
|
event.keyCode = static_cast<std::int32_t>(keyCode);
|
|
|
|
|
event.repeat = repeat;
|
2026-04-05 05:58:05 +08:00
|
|
|
event.modifiers.shift = shift;
|
|
|
|
|
event.modifiers.control = control;
|
|
|
|
|
event.modifiers.alt = alt;
|
|
|
|
|
event.modifiers.super = super;
|
2026-04-05 04:55:25 +08:00
|
|
|
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<const UIDrawCommand*> CollectTextCommands(const XCEngine::UI::UIDrawData& drawData) {
|
|
|
|
|
std::vector<const UIDrawCommand*> 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<const UIDrawCommand*> 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);
|
2026-04-05 06:03:15 +08:00
|
|
|
|
|
|
|
|
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);
|
2026-04-05 04:55:25 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 12:10:55 +08:00
|
|
|
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<std::string> 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<std::string>({ "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<std::string>({ "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<std::string>({
|
|
|
|
|
"demo.text.edit.agentPrompt",
|
|
|
|
|
"demo.text.edit.agentPrompt",
|
|
|
|
|
"demo.text.edit.agentPrompt" }));
|
|
|
|
|
EXPECT_TRUE(runtime.DrainPendingCommandIds().empty());
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 04:55:25 +08:00
|
|
|
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 = "</Column>\n</View>";
|
|
|
|
|
const std::size_t insertPosition = originalContents.rfind(marker);
|
|
|
|
|
ASSERT_NE(insertPosition, std::string::npos);
|
|
|
|
|
|
|
|
|
|
const std::string injectedNode =
|
|
|
|
|
" <Text id=\"autoReloadProbe\" text=\"Auto Reload Probe\" style=\"Meta\" />\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");
|
|
|
|
|
}
|
2026-04-05 05:14:16 +08:00
|
|
|
|
|
|
|
|
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");
|
2026-04-05 05:58:05 +08:00
|
|
|
EXPECT_NE(FindTextCommand(typedFrame.drawData, "1"), nullptr);
|
|
|
|
|
EXPECT_NE(FindTextCommand(typedFrame.drawData, "2"), nullptr);
|
2026-04-05 05:14:16 +08:00
|
|
|
EXPECT_NE(FindTextCommand(typedFrame.drawData, "OK"), nullptr);
|
|
|
|
|
EXPECT_NE(FindTextCommand(typedFrame.drawData, "X"), nullptr);
|
2026-04-05 06:03:15 +08:00
|
|
|
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");
|
2026-04-05 05:14:16 +08:00
|
|
|
|
2026-04-05 05:58:05 +08:00
|
|
|
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");
|
2026-04-05 06:03:15 +08:00
|
|
|
EXPECT_NE(FindTextCommand(indentedFrame.drawData, "OK"), nullptr);
|
|
|
|
|
EXPECT_NE(FindTextCommand(indentedFrame.drawData, " X"), nullptr);
|
2026-04-05 05:58:05 +08:00
|
|
|
|
2026-04-05 06:03:15 +08:00
|
|
|
const UIDrawCommand* firstLineText = FindTextCommand(indentedFrame.drawData, "OK");
|
|
|
|
|
ASSERT_NE(firstLineText, nullptr);
|
2026-04-05 05:58:05 +08:00
|
|
|
|
2026-04-05 06:03:15 +08:00
|
|
|
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);
|
2026-04-05 05:58:05 +08:00
|
|
|
|
2026-04-05 06:03:15 +08:00
|
|
|
XCEngine::Editor::XCUIBackend::XCUIDemoInputState firstLineReleased = BuildInputState();
|
|
|
|
|
firstLineReleased.pointerPosition = firstLinePressed.pointerPosition;
|
|
|
|
|
firstLineReleased.pointerReleased = true;
|
|
|
|
|
const auto& caretFrame = runtime.Update(firstLineReleased);
|
2026-04-05 05:58:05 +08:00
|
|
|
|
|
|
|
|
ASSERT_TRUE(caretFrame.stats.documentsReady);
|
|
|
|
|
EXPECT_EQ(caretFrame.stats.focusedElementId, "sessionNotes");
|
|
|
|
|
|
2026-04-05 06:03:15 +08:00
|
|
|
XCEngine::Editor::XCUIBackend::XCUIDemoInputState editInput = BuildInputState();
|
|
|
|
|
editInput.events.push_back(MakeCharacterEvent('!'));
|
|
|
|
|
const auto& editedFrame = runtime.Update(editInput);
|
2026-04-05 05:14:16 +08:00
|
|
|
|
|
|
|
|
ASSERT_TRUE(editedFrame.stats.documentsReady);
|
2026-04-05 05:58:05 +08:00
|
|
|
EXPECT_NE(FindTextCommand(editedFrame.drawData, "!OK"), nullptr);
|
2026-04-05 06:03:15 +08:00
|
|
|
EXPECT_NE(FindTextCommand(editedFrame.drawData, " X"), nullptr);
|
2026-04-05 05:14:16 +08:00
|
|
|
EXPECT_EQ(editedFrame.stats.focusedElementId, "sessionNotes");
|
|
|
|
|
}
|