Files
XCEngine/tests/NewEditor/test_xcui_demo_runtime.cpp

618 lines
25 KiB
C++

#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;
}
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<std::int32_t>(keyCode);
event.repeat = repeat;
event.modifiers.shift = shift;
event.modifiers.control = control;
event.modifiers.alt = alt;
event.modifiers.super = super;
return event;
}
UIInputEvent MakePointerButtonEvent(
UIInputEventType type,
const XCEngine::UI::UIPoint& position,
XCEngine::UI::UIPointerButton button = XCEngine::UI::UIPointerButton::Left) {
UIInputEvent event = {};
event.type = type;
event.pointerButton = button;
event.position = position;
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);
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<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, DrainPendingCommandIdsCapturesShortcutCommands) {
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::Editor::XCUIBackend::XCUIDemoInputState shortcutInput = BuildInputState();
shortcutInput.events.push_back(MakeKeyDownEvent(XCEngine::Input::KeyCode::P, false, false, true));
const auto& shortcutFrame = runtime.Update(shortcutInput);
ASSERT_TRUE(shortcutFrame.stats.documentsReady);
EXPECT_TRUE(shortcutFrame.stats.accentEnabled);
EXPECT_EQ(shortcutFrame.stats.lastCommandId, "demo.toggleAccent");
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());
}
TEST(NewEditorXCUIDemoRuntimeTest, DrainPendingCommandIdsPreserveMixedPointerTextAndShortcutOrder) {
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 mixedInput = BuildInputState();
mixedInput.pointerPosition = promptCenter;
mixedInput.events.push_back(MakePointerButtonEvent(UIInputEventType::PointerButtonDown, promptCenter));
mixedInput.events.push_back(MakePointerButtonEvent(UIInputEventType::PointerButtonUp, promptCenter));
mixedInput.events.push_back(MakeCharacterEvent('A'));
mixedInput.events.push_back(MakeKeyDownEvent(XCEngine::Input::KeyCode::P, false, false, true));
const auto& mixedFrame = runtime.Update(mixedInput);
ASSERT_TRUE(mixedFrame.stats.documentsReady);
EXPECT_EQ(mixedFrame.stats.focusedElementId, "agentPrompt");
EXPECT_TRUE(mixedFrame.stats.accentEnabled);
EXPECT_EQ(mixedFrame.stats.lastCommandId, "demo.toggleAccent");
EXPECT_NE(FindTextCommand(mixedFrame.drawData, "A"), nullptr);
EXPECT_EQ(
runtime.DrainPendingCommandIds(),
std::vector<std::string>({
"demo.activate.agentPrompt",
"demo.text.edit.agentPrompt",
"demo.toggleAccent" }));
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 = "</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");
}
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");
}