feat(xcui): close scroll view validation loop

This commit is contained in:
2026-04-06 13:13:17 +08:00
parent b14a4fb7bb
commit 0804052d6f
11 changed files with 477 additions and 2 deletions

View File

@@ -1,10 +1,12 @@
add_subdirectory(keyboard_focus)
add_subdirectory(pointer_states)
add_subdirectory(scroll_view)
add_subdirectory(shortcut_scope)
add_custom_target(editor_ui_input_integration_tests
DEPENDS
editor_ui_input_keyboard_focus_validation
editor_ui_input_pointer_states_validation
editor_ui_input_scroll_view_validation
editor_ui_input_shortcut_scope_validation
)

View File

@@ -0,0 +1,35 @@
set(EDITOR_UI_INPUT_SCROLL_VIEW_RESOURCES
View.xcui
${CMAKE_SOURCE_DIR}/tests/UI/Editor/integration/shared/themes/editor_validation.xctheme
)
add_executable(editor_ui_input_scroll_view_validation WIN32
main.cpp
${EDITOR_UI_INPUT_SCROLL_VIEW_RESOURCES}
)
target_include_directories(editor_ui_input_scroll_view_validation PRIVATE
${CMAKE_SOURCE_DIR}/tests/UI/Editor/integration/shared/src
${CMAKE_SOURCE_DIR}/engine/include
)
target_compile_definitions(editor_ui_input_scroll_view_validation PRIVATE
UNICODE
_UNICODE
)
if(MSVC)
target_compile_options(editor_ui_input_scroll_view_validation PRIVATE /utf-8 /FS)
set_property(TARGET editor_ui_input_scroll_view_validation PROPERTY
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
endif()
target_link_libraries(editor_ui_input_scroll_view_validation PRIVATE
editor_ui_integration_host
)
set_target_properties(editor_ui_input_scroll_view_validation PROPERTIES
OUTPUT_NAME "XCUIEditorInputScrollViewValidation"
)
source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" FILES View.xcui)

View File

@@ -0,0 +1,61 @@
<View
name="EditorInputScrollView"
theme="../../shared/themes/editor_validation.xctheme">
<Column padding="20" gap="12">
<Card
title="功能ScrollView 滚动 / clip / overflow"
subtitle="只检查滚轮滚动、裁剪、overflow 与 target 路由,不检查业务面板"
tone="accent"
height="118">
<Column gap="6">
<Text text="1. 把鼠标放到下方日志区内滚动滚轮:内容应上下移动,右下角 Scroll target 应落到 validation-scroll。" />
<Text text="2. 连续向下滚到末尾再继续滚Offset 应被 clampResult 应显示 Scroll delta clamped to current offset。" />
<Text text="3. 把鼠标移到日志区外再滚动日志位置不应变化Result 应显示 No hovered ScrollView。" />
<Text text="4. 这个场景只验证 ScrollView 基础能力,不验证 editor 业务面板。" />
</Column>
</Card>
<Card
title="Scrollable Log"
subtitle="wheel inside this viewport"
height="fill">
<ScrollView id="validation-scroll" height="fill">
<Column gap="8">
<Text text="Line 01 - Scroll validation log" />
<Text text="Line 02 - Scroll validation log" />
<Text text="Line 03 - Scroll validation log" />
<Text text="Line 04 - Scroll validation log" />
<Text text="Line 05 - Scroll validation log" />
<Text text="Line 06 - Scroll validation log" />
<Text text="Line 07 - Scroll validation log" />
<Text text="Line 08 - Scroll validation log" />
<Text text="Line 09 - Scroll validation log" />
<Text text="Line 10 - Scroll validation log" />
<Text text="Line 11 - Scroll validation log" />
<Text text="Line 12 - Scroll validation log" />
<Text text="Line 13 - Scroll validation log" />
<Text text="Line 14 - Scroll validation log" />
<Text text="Line 15 - Scroll validation log" />
<Text text="Line 16 - Scroll validation log" />
<Text text="Line 17 - Scroll validation log" />
<Text text="Line 18 - Scroll validation log" />
<Text text="Line 19 - Scroll validation log" />
<Text text="Line 20 - Scroll validation log" />
<Text text="Line 21 - Scroll validation log" />
<Text text="Line 22 - Scroll validation log" />
<Text text="Line 23 - Scroll validation log" />
<Text text="Line 24 - Scroll validation log" />
</Column>
</ScrollView>
</Card>
<Card
title="Outside Area"
subtitle="wheel here should not move the log"
height="84">
<Column gap="8">
<Text text="把鼠标移到这个区域再滚动,用来检查 No hovered ScrollView。" />
</Column>
</Card>
</Column>
</View>

View File

@@ -0,0 +1,8 @@
#include "Application.h"
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow) {
return XCEngine::Tests::EditorUI::RunEditorUIValidationApp(
hInstance,
nCmdShow,
"editor.input.scroll_view");
}

View File

@@ -530,6 +530,7 @@ void Application::AppendRuntimeOverlay(UIDrawData& drawData, float width, float
if (authoredMode) {
const auto& inputDebug = m_documentHost.GetInputDebugSnapshot();
const auto& scrollDebug = m_documentHost.GetScrollDebugSnapshot();
detailLines.push_back(
"Hover | Focus: " +
ExtractStateKeyTail(inputDebug.hoveredStateKey) +
@@ -601,6 +602,25 @@ void Application::AppendRuntimeOverlay(UIDrawData& drawData, float width, float
"Last event result: " +
(inputDebug.lastResult.empty() ? std::string("n/a") : inputDebug.lastResult));
}
detailLines.push_back(
"Scroll target | Primary: " +
ExtractStateKeyTail(scrollDebug.lastTargetStateKey) +
" | " +
ExtractStateKeyTail(scrollDebug.primaryTargetStateKey));
detailLines.push_back(
"Scroll offset B/A: " +
FormatFloat(scrollDebug.lastOffsetBefore) +
" -> " +
FormatFloat(scrollDebug.lastOffsetAfter) +
" | overflow " +
FormatFloat(scrollDebug.lastOverflow));
detailLines.push_back(
"Scroll H/T: " +
std::to_string(scrollDebug.handledWheelEventCount) +
"/" +
std::to_string(scrollDebug.totalWheelEventCount) +
" | " +
(scrollDebug.lastResult.empty() ? std::string("n/a") : scrollDebug.lastResult));
}
if (m_autoScreenshot.HasPendingCapture()) {

View File

@@ -24,8 +24,8 @@ fs::path RepoRelative(const char* relativePath) {
return (RepoRootPath() / relativePath).lexically_normal();
}
const std::array<EditorValidationScenario, 6>& GetEditorValidationScenarios() {
static const std::array<EditorValidationScenario, 6> scenarios = { {
const std::array<EditorValidationScenario, 7>& GetEditorValidationScenarios() {
static const std::array<EditorValidationScenario, 7> scenarios = { {
{
"editor.input.keyboard_focus",
UIValidationDomain::Editor,
@@ -44,6 +44,15 @@ const std::array<EditorValidationScenario, 6>& GetEditorValidationScenarios() {
RepoRelative("tests/UI/Editor/integration/shared/themes/editor_validation.xctheme"),
RepoRelative("tests/UI/Editor/integration/input/pointer_states/captures")
},
{
"editor.input.scroll_view",
UIValidationDomain::Editor,
"input",
"Editor Input | Scroll View",
RepoRelative("tests/UI/Editor/integration/input/scroll_view/View.xcui"),
RepoRelative("tests/UI/Editor/integration/shared/themes/editor_validation.xctheme"),
RepoRelative("tests/UI/Editor/integration/input/scroll_view/captures")
},
{
"editor.input.shortcut_scope",
UIValidationDomain::Editor,

View File

@@ -15,6 +15,7 @@ using XCEngine::Tests::EditorUI::UIValidationDomain;
TEST(EditorValidationRegistryTest, KnownEditorValidationScenariosResolveToExistingResources) {
const auto* pointerScenario = FindEditorValidationScenario("editor.input.pointer_states");
const auto* keyboardScenario = FindEditorValidationScenario("editor.input.keyboard_focus");
const auto* scrollScenario = FindEditorValidationScenario("editor.input.scroll_view");
const auto* shortcutScenario = FindEditorValidationScenario("editor.input.shortcut_scope");
const auto* splitterScenario = FindEditorValidationScenario("editor.layout.splitter_resize");
const auto* tabStripScenario = FindEditorValidationScenario("editor.layout.tab_strip_selection");
@@ -22,18 +23,21 @@ TEST(EditorValidationRegistryTest, KnownEditorValidationScenariosResolveToExisti
ASSERT_NE(pointerScenario, nullptr);
ASSERT_NE(keyboardScenario, nullptr);
ASSERT_NE(scrollScenario, nullptr);
ASSERT_NE(shortcutScenario, nullptr);
ASSERT_NE(splitterScenario, nullptr);
ASSERT_NE(tabStripScenario, nullptr);
ASSERT_NE(workspaceScenario, nullptr);
EXPECT_EQ(pointerScenario->domain, UIValidationDomain::Editor);
EXPECT_EQ(keyboardScenario->domain, UIValidationDomain::Editor);
EXPECT_EQ(scrollScenario->domain, UIValidationDomain::Editor);
EXPECT_EQ(shortcutScenario->domain, UIValidationDomain::Editor);
EXPECT_EQ(splitterScenario->domain, UIValidationDomain::Editor);
EXPECT_EQ(tabStripScenario->domain, UIValidationDomain::Editor);
EXPECT_EQ(workspaceScenario->domain, UIValidationDomain::Editor);
EXPECT_EQ(pointerScenario->categoryId, "input");
EXPECT_EQ(keyboardScenario->categoryId, "input");
EXPECT_EQ(scrollScenario->categoryId, "input");
EXPECT_EQ(shortcutScenario->categoryId, "input");
EXPECT_EQ(splitterScenario->categoryId, "layout");
EXPECT_EQ(tabStripScenario->categoryId, "layout");
@@ -42,6 +46,8 @@ TEST(EditorValidationRegistryTest, KnownEditorValidationScenariosResolveToExisti
EXPECT_TRUE(std::filesystem::exists(pointerScenario->themePath));
EXPECT_TRUE(std::filesystem::exists(keyboardScenario->documentPath));
EXPECT_TRUE(std::filesystem::exists(keyboardScenario->themePath));
EXPECT_TRUE(std::filesystem::exists(scrollScenario->documentPath));
EXPECT_TRUE(std::filesystem::exists(scrollScenario->themePath));
EXPECT_TRUE(std::filesystem::exists(shortcutScenario->documentPath));
EXPECT_TRUE(std::filesystem::exists(shortcutScenario->themePath));
EXPECT_TRUE(std::filesystem::exists(splitterScenario->documentPath));

View File

@@ -1,4 +1,5 @@
set(RUNTIME_UI_TEST_SOURCES
${CMAKE_SOURCE_DIR}/tests/UI/Runtime/unit/test_ui_runtime_scroll_view.cpp
${CMAKE_SOURCE_DIR}/tests/UI/Runtime/unit/test_ui_runtime_shortcut_scope.cpp
${CMAKE_SOURCE_DIR}/tests/UI/Runtime/unit/test_ui_runtime_splitter_validation.cpp
${CMAKE_SOURCE_DIR}/tests/UI/Runtime/unit/test_ui_runtime_tab_strip.cpp

View File

@@ -0,0 +1,331 @@
#include <gtest/gtest.h>
#include <XCEngine/UI/Runtime/UIScreenDocumentHost.h>
#include <XCEngine/UI/Runtime/UIScreenPlayer.h>
#include <chrono>
#include <filesystem>
#include <fstream>
#include <limits>
#include <string>
namespace {
using XCEngine::UI::UIDrawCommand;
using XCEngine::UI::UIDrawCommandType;
using XCEngine::UI::UIDrawData;
using XCEngine::UI::UIDrawList;
using XCEngine::UI::UIInputEvent;
using XCEngine::UI::UIInputEventType;
using XCEngine::UI::UIPoint;
using XCEngine::UI::UIRect;
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 errorCode = {};
fs::remove(m_path, errorCode);
}
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 = 1u) {
UIScreenFrameInput input = {};
input.viewportRect = UIRect(0.0f, 0.0f, 520.0f, 340.0f);
input.frameIndex = frameIndex;
input.focused = true;
return input;
}
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;
}
std::size_t CountCommandsOfType(
const UIDrawData& drawData,
UIDrawCommandType type) {
std::size_t count = 0u;
for (const UIDrawList& drawList : drawData.GetDrawLists()) {
for (const UIDrawCommand& command : drawList.GetCommands()) {
if (command.type == type) {
++count;
}
}
}
return count;
}
bool RectContainsPoint(const UIRect& rect, const UIPoint& point) {
return point.x >= rect.x &&
point.x <= rect.x + rect.width &&
point.y >= rect.y &&
point.y <= rect.y + rect.height;
}
bool TryFindSmallestFilledRectContainingPoint(
const UIDrawData& drawData,
const UIPoint& point,
UIRect& outRect) {
bool found = false;
float bestArea = (std::numeric_limits<float>::max)();
for (const UIDrawList& drawList : drawData.GetDrawLists()) {
for (const UIDrawCommand& command : drawList.GetCommands()) {
if (command.type != UIDrawCommandType::FilledRect ||
!RectContainsPoint(command.rect, point)) {
continue;
}
const float area = command.rect.width * command.rect.height;
if (!found || area < bestArea) {
outRect = command.rect;
bestArea = area;
found = true;
}
}
}
return found;
}
bool TryFindFilledRectForText(
const UIDrawData& drawData,
const std::string& text,
UIRect& outRect) {
const UIDrawCommand* textCommand = FindTextCommand(drawData, text);
return textCommand != nullptr &&
TryFindSmallestFilledRectContainingPoint(drawData, textCommand->position, outRect);
}
UIPoint GetRectCenter(const UIRect& rect) {
return UIPoint(rect.x + rect.width * 0.5f, rect.y + rect.height * 0.5f);
}
} // namespace
TEST(UIRuntimeScrollViewTest, ScrollViewClipsOverflowingChildrenAndPersistsWheelOffset) {
TempFileScope viewFile(
"xcui_runtime_scroll_view",
".xcui",
"<View name=\"Scroll View Test\">\n"
" <Column padding=\"18\" gap=\"10\">\n"
" <Card title=\"Console\" subtitle=\"Scroll smoke\" height=\"200\">\n"
" <ScrollView id=\"log-scroll\" height=\"fill\">\n"
" <Column gap=\"8\">\n"
" <Text text=\"Line 01\" />\n"
" <Text text=\"Line 02\" />\n"
" <Text text=\"Line 03\" />\n"
" <Text text=\"Line 04\" />\n"
" <Text text=\"Line 05\" />\n"
" <Text text=\"Line 06\" />\n"
" <Text text=\"Line 07\" />\n"
" <Text text=\"Line 08\" />\n"
" <Text text=\"Line 09\" />\n"
" <Text text=\"Line 10\" />\n"
" <Text text=\"Line 11\" />\n"
" <Text text=\"Line 12\" />\n"
" </Column>\n"
" </ScrollView>\n"
" </Card>\n"
" </Column>\n"
"</View>\n");
UIDocumentScreenHost host = {};
UIScreenPlayer player(host);
ASSERT_TRUE(player.Load(BuildScreenAsset(viewFile.Path(), "runtime.scroll.view")));
UIScreenFrameInput firstInput = BuildInputState(1u);
const auto& firstFrame = player.Update(firstInput);
const auto* line01Before = FindTextCommand(firstFrame.drawData, "Line 01");
ASSERT_NE(line01Before, nullptr);
EXPECT_GT(CountCommandsOfType(firstFrame.drawData, UIDrawCommandType::PushClipRect), 0u);
EXPECT_GT(CountCommandsOfType(firstFrame.drawData, UIDrawCommandType::PopClipRect), 0u);
UIScreenFrameInput secondInput = BuildInputState(2u);
UIInputEvent wheelEvent = {};
wheelEvent.type = UIInputEventType::PointerWheel;
wheelEvent.position = UIPoint(90.0f, 130.0f);
wheelEvent.wheelDelta = -120.0f;
secondInput.events.push_back(wheelEvent);
const auto& secondFrame = player.Update(secondInput);
const auto* line01After = FindTextCommand(secondFrame.drawData, "Line 01");
ASSERT_NE(line01After, nullptr);
EXPECT_LT(line01After->position.y, line01Before->position.y);
const auto& scrollDebug = host.GetScrollDebugSnapshot();
EXPECT_NE(scrollDebug.lastTargetStateKey.find("/log-scroll"), std::string::npos);
EXPECT_EQ(scrollDebug.lastResult, "Handled");
EXPECT_EQ(scrollDebug.handledWheelEventCount, 1u);
EXPECT_LT(scrollDebug.lastOffsetBefore, scrollDebug.lastOffsetAfter);
UIScreenFrameInput thirdInput = BuildInputState(3u);
const auto& thirdFrame = player.Update(thirdInput);
const auto* line01Persisted = FindTextCommand(thirdFrame.drawData, "Line 01");
ASSERT_NE(line01Persisted, nullptr);
EXPECT_FLOAT_EQ(line01Persisted->position.y, line01After->position.y);
}
TEST(UIRuntimeScrollViewTest, WheelOutsideScrollViewportDoesNotChangeContentOffset) {
TempFileScope viewFile(
"xcui_runtime_scroll_view_outside",
".xcui",
"<View name=\"Scroll Outside Test\">\n"
" <Column padding=\"18\" gap=\"10\">\n"
" <Card title=\"Console\" subtitle=\"Outside wheel should do nothing\" height=\"200\">\n"
" <ScrollView id=\"log-scroll\" height=\"fill\">\n"
" <Column gap=\"8\">\n"
" <Text text=\"Line 01\" />\n"
" <Text text=\"Line 02\" />\n"
" <Text text=\"Line 03\" />\n"
" <Text text=\"Line 04\" />\n"
" <Text text=\"Line 05\" />\n"
" <Text text=\"Line 06\" />\n"
" <Text text=\"Line 07\" />\n"
" <Text text=\"Line 08\" />\n"
" <Text text=\"Line 09\" />\n"
" <Text text=\"Line 10\" />\n"
" <Text text=\"Line 11\" />\n"
" <Text text=\"Line 12\" />\n"
" </Column>\n"
" </ScrollView>\n"
" </Card>\n"
" <Card title=\"Outside Area\" subtitle=\"not scrollable\" height=\"72\" />\n"
" </Column>\n"
"</View>\n");
UIDocumentScreenHost host = {};
UIScreenPlayer player(host);
ASSERT_TRUE(player.Load(BuildScreenAsset(viewFile.Path(), "runtime.scroll.outside")));
UIScreenFrameInput firstInput = BuildInputState(1u);
const auto& firstFrame = player.Update(firstInput);
const auto* line01Before = FindTextCommand(firstFrame.drawData, "Line 01");
ASSERT_NE(line01Before, nullptr);
UIScreenFrameInput secondInput = BuildInputState(2u);
UIInputEvent wheelEvent = {};
wheelEvent.type = UIInputEventType::PointerWheel;
wheelEvent.position = UIPoint(110.0f, 300.0f);
wheelEvent.wheelDelta = -120.0f;
secondInput.events.push_back(wheelEvent);
const auto& secondFrame = player.Update(secondInput);
const auto* line01After = FindTextCommand(secondFrame.drawData, "Line 01");
ASSERT_NE(line01After, nullptr);
EXPECT_FLOAT_EQ(line01After->position.y, line01Before->position.y);
const auto& scrollDebug = host.GetScrollDebugSnapshot();
EXPECT_EQ(scrollDebug.lastResult, "No hovered ScrollView");
EXPECT_EQ(scrollDebug.handledWheelEventCount, 0u);
}
TEST(UIRuntimeScrollViewTest, NestedScrollViewRoutesWheelToDeepestHoveredTarget) {
TempFileScope viewFile(
"xcui_runtime_nested_scroll_view",
".xcui",
"<View name=\"Nested Scroll View Test\">\n"
" <Column padding=\"18\" gap=\"10\">\n"
" <ScrollView id=\"outer-scroll\" height=\"260\">\n"
" <Column gap=\"8\">\n"
" <Text text=\"Outer 01\" />\n"
" <Text text=\"Outer 02\" />\n"
" <Text text=\"Outer 03\" />\n"
" <Card title=\"Nested Panel\" subtitle=\"inner scroll host\" height=\"180\">\n"
" <ScrollView id=\"inner-scroll\" height=\"fill\">\n"
" <Column gap=\"8\">\n"
" <Text text=\"Inner 01\" />\n"
" <Text text=\"Inner 02\" />\n"
" <Text text=\"Inner 03\" />\n"
" <Text text=\"Inner 04\" />\n"
" <Text text=\"Inner 05\" />\n"
" <Text text=\"Inner 06\" />\n"
" <Text text=\"Inner 07\" />\n"
" <Text text=\"Inner 08\" />\n"
" <Text text=\"Inner 09\" />\n"
" <Text text=\"Inner 10\" />\n"
" <Text text=\"Inner 11\" />\n"
" <Text text=\"Inner 12\" />\n"
" </Column>\n"
" </ScrollView>\n"
" </Card>\n"
" <Text text=\"Outer 04\" />\n"
" <Text text=\"Outer 05\" />\n"
" <Text text=\"Outer 06\" />\n"
" <Text text=\"Outer 07\" />\n"
" <Text text=\"Outer 08\" />\n"
" <Text text=\"Outer 09\" />\n"
" </Column>\n"
" </ScrollView>\n"
" </Column>\n"
"</View>\n");
UIDocumentScreenHost host = {};
UIScreenPlayer player(host);
ASSERT_TRUE(player.Load(BuildScreenAsset(viewFile.Path(), "runtime.scroll.nested")));
UIScreenFrameInput firstInput = BuildInputState(1u);
const auto& firstFrame = player.Update(firstInput);
const auto* innerBefore = FindTextCommand(firstFrame.drawData, "Inner 01");
ASSERT_NE(innerBefore, nullptr);
UIRect innerRect = {};
ASSERT_TRUE(TryFindFilledRectForText(firstFrame.drawData, "Inner 01", innerRect));
const UIPoint wheelPoint = GetRectCenter(innerRect);
UIScreenFrameInput secondInput = BuildInputState(2u);
UIInputEvent wheelEvent = {};
wheelEvent.type = UIInputEventType::PointerWheel;
wheelEvent.position = wheelPoint;
wheelEvent.wheelDelta = -120.0f;
secondInput.events.push_back(wheelEvent);
const auto& secondFrame = player.Update(secondInput);
const auto* innerAfter = FindTextCommand(secondFrame.drawData, "Inner 01");
ASSERT_NE(innerAfter, nullptr);
EXPECT_LT(innerAfter->position.y, innerBefore->position.y);
const auto& scrollDebug = host.GetScrollDebugSnapshot();
EXPECT_NE(scrollDebug.lastTargetStateKey.find("/inner-scroll"), std::string::npos);
EXPECT_NE(scrollDebug.primaryTargetStateKey.find("/outer-scroll"), std::string::npos);
EXPECT_EQ(scrollDebug.lastResult, "Handled");
EXPECT_GT(scrollDebug.lastOverflow, 0.0f);
}

View File

@@ -119,6 +119,7 @@ Runtime 的集成测试结构与 Editor 保持同一规范,但宿主职责必
- `editor.input.keyboard_focus`
- `editor.input.pointer_states`
- `editor.input.scroll_view`
- `editor.input.shortcut_scope`
- `editor.layout.splitter_resize`
- `editor.layout.tab_strip_selection`