246 lines
9.5 KiB
C++
246 lines
9.5 KiB
C++
|
|
#include <gtest/gtest.h>
|
||
|
|
|
||
|
|
#include <XCEngine/Input/InputTypes.h>
|
||
|
|
#include <XCEngine/UI/Runtime/UIScreenDocumentHost.h>
|
||
|
|
#include <XCEngine/UI/Runtime/UIScreenPlayer.h>
|
||
|
|
|
||
|
|
#include <chrono>
|
||
|
|
#include <filesystem>
|
||
|
|
#include <fstream>
|
||
|
|
#include <string>
|
||
|
|
|
||
|
|
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
|
||
|
|
"<View name=\"Runtime TabStrip Test\">\n"
|
||
|
|
" <Column padding=\"18\" gap=\"10\">\n"
|
||
|
|
" <TabStrip id=\"workspace-tabs\" tabHeaderHeight=\"34\" tabMinWidth=\"84\">\n"
|
||
|
|
" <Tab id=\"tab-inspector\" label=\"Inspector\" selected=\"true\">\n"
|
||
|
|
" <Card title=\"Inspector Content\">\n"
|
||
|
|
" <Text text=\"Selected: Inspector\" />\n"
|
||
|
|
" </Card>\n"
|
||
|
|
" </Tab>\n"
|
||
|
|
" <Tab id=\"tab-console\" label=\"Console\">\n"
|
||
|
|
" <Card title=\"Console Content\">\n"
|
||
|
|
" <Text text=\"Selected: Console\" />\n"
|
||
|
|
" </Card>\n"
|
||
|
|
" </Tab>\n"
|
||
|
|
" <Tab id=\"tab-profiler\" label=\"Profiler\">\n"
|
||
|
|
" <Card title=\"Profiler Content\">\n"
|
||
|
|
" <Text text=\"Selected: Profiler\" />\n"
|
||
|
|
" </Card>\n"
|
||
|
|
" </Tab>\n"
|
||
|
|
" </TabStrip>\n"
|
||
|
|
" </Column>\n"
|
||
|
|
"</View>\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<std::int32_t>(keyCode);
|
||
|
|
return event;
|
||
|
|
}
|
||
|
|
|
||
|
|
} // namespace
|
||
|
|
|
||
|
|
TEST(UIRuntimeTabStripValidationTest, EmptyTabStripProducesExplicitFrameError) {
|
||
|
|
TempFileScope viewFile(
|
||
|
|
"xcui_runtime_invalid_tab_strip",
|
||
|
|
".xcui",
|
||
|
|
"<View name=\"Invalid TabStrip Test\">\n"
|
||
|
|
" <Column padding=\"16\" gap=\"10\">\n"
|
||
|
|
" <TabStrip id=\"broken-tabs\" />\n"
|
||
|
|
" </Column>\n"
|
||
|
|
"</View>\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);
|
||
|
|
}
|