diff --git a/.gitignore b/.gitignore
index 9960f95..036b9c9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,8 @@
build/
build-x86/
build-arm64/
+tests/apps/WinUI3Sample/bin/
+tests/apps/WinUI3Sample/obj/
# IDE files
.vs/
diff --git a/CMakeLists.txt b/CMakeLists.txt
index cc1ea7f..455ba4b 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -303,6 +303,32 @@ add_custom_target(wpf_sample_app ALL
file(TO_NATIVE_PATH "${WPF_SAMPLE_OUTPUT_DIR}/WpfSample.exe" WPF_SAMPLE_EXE_NATIVE)
string(REPLACE "\\" "\\\\" WPF_SAMPLE_EXE_ESCAPED "${WPF_SAMPLE_EXE_NATIVE}")
+# WinUI3 sample app used by integration tests
+set(WINUI3_SAMPLE_PROJECT "${CMAKE_SOURCE_DIR}/tests/apps/WinUI3Sample/WinUI3Sample.csproj")
+set(WINUI3_SAMPLE_OUTPUT_DIR "${CMAKE_BINARY_DIR}/tests/apps/WinUI3Sample")
+set(WINUI3_SAMPLE_MSBUILD_ARGS "")
+if(DEFINED ENV{VSINSTALLDIR} AND EXISTS "$ENV{VSINSTALLDIR}/MSBuild/Microsoft/VisualStudio/v18.0/AppxPackage/Microsoft.Build.Packaging.Pri.Tasks.dll")
+ list(APPEND WINUI3_SAMPLE_MSBUILD_ARGS
+ "-p:MSBuildExtensionsPath=$ENV{VSINSTALLDIR}/MSBuild")
+endif()
+add_custom_target(winui3_sample_app ALL
+ COMMAND dotnet publish "${WINUI3_SAMPLE_PROJECT}"
+ -c Release -p:Platform=x64 -r win-x64 --self-contained true
+ -o "${WINUI3_SAMPLE_OUTPUT_DIR}"
+ ${WINUI3_SAMPLE_MSBUILD_ARGS}
+ COMMAND ${CMAKE_COMMAND} -E copy_if_different
+ "${CMAKE_SOURCE_DIR}/tests/apps/WinUI3Sample/obj/x64/Release/net10.0-windows10.0.19041.0/win-x64/App.xbf"
+ "${WINUI3_SAMPLE_OUTPUT_DIR}/App.xbf"
+ COMMAND ${CMAKE_COMMAND} -E copy_if_different
+ "${CMAKE_SOURCE_DIR}/tests/apps/WinUI3Sample/obj/x64/Release/net10.0-windows10.0.19041.0/win-x64/MainWindow.xbf"
+ "${WINUI3_SAMPLE_OUTPUT_DIR}/MainWindow.xbf"
+ BYPRODUCTS "${WINUI3_SAMPLE_OUTPUT_DIR}/WinUI3Sample.exe"
+ WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
+ COMMENT "Publishing WinUI3 sample app"
+)
+file(TO_NATIVE_PATH "${WINUI3_SAMPLE_OUTPUT_DIR}/WinUI3Sample.exe" WINUI3_SAMPLE_EXE_NATIVE)
+string(REPLACE "\\" "\\\\" WINUI3_SAMPLE_EXE_ESCAPED "${WINUI3_SAMPLE_EXE_NATIVE}")
+
# Integration tests — require a running Notepad instance
add_executable(lvt_integration_tests
tests/integration_tests.cpp
@@ -312,6 +338,7 @@ target_include_directories(lvt_integration_tests PRIVATE src)
target_compile_definitions(lvt_integration_tests PRIVATE
WIN32_LEAN_AND_MEAN NOMINMAX WINRT_LEAN_AND_MEAN UNICODE _UNICODE
WPF_SAMPLE_EXE_PATH="${WPF_SAMPLE_EXE_ESCAPED}"
+ WINUI3_SAMPLE_EXE_PATH="${WINUI3_SAMPLE_EXE_ESCAPED}"
)
target_link_libraries(lvt_integration_tests PRIVATE
GTest::gtest GTest::gtest_main
@@ -319,7 +346,7 @@ target_link_libraries(lvt_integration_tests PRIVATE
comctl32
)
target_link_options(lvt_integration_tests PRIVATE "/MANIFEST:NO")
-add_dependencies(lvt_integration_tests wpf_sample_app)
+add_dependencies(lvt_integration_tests wpf_sample_app winui3_sample_app)
add_test(NAME integration_tests COMMAND lvt_integration_tests)
# Chromium plugin tests — DOM JSON format and native messaging protocol
diff --git a/tests/apps/WinUI3Sample/App.xaml b/tests/apps/WinUI3Sample/App.xaml
new file mode 100644
index 0000000..74ab9ee
--- /dev/null
+++ b/tests/apps/WinUI3Sample/App.xaml
@@ -0,0 +1,5 @@
+
+
diff --git a/tests/apps/WinUI3Sample/App.xaml.cs b/tests/apps/WinUI3Sample/App.xaml.cs
new file mode 100644
index 0000000..49a4f44
--- /dev/null
+++ b/tests/apps/WinUI3Sample/App.xaml.cs
@@ -0,0 +1,19 @@
+using Microsoft.UI.Xaml;
+
+namespace WinUI3Sample;
+
+public partial class App : Application
+{
+ private Window? window;
+
+ public App()
+ {
+ InitializeComponent();
+ }
+
+ protected override void OnLaunched(LaunchActivatedEventArgs args)
+ {
+ window = new MainWindow();
+ window.Activate();
+ }
+}
diff --git a/tests/apps/WinUI3Sample/MainWindow.xaml b/tests/apps/WinUI3Sample/MainWindow.xaml
new file mode 100644
index 0000000..c38fb40
--- /dev/null
+++ b/tests/apps/WinUI3Sample/MainWindow.xaml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/apps/WinUI3Sample/MainWindow.xaml.cs b/tests/apps/WinUI3Sample/MainWindow.xaml.cs
new file mode 100644
index 0000000..b5164a0
--- /dev/null
+++ b/tests/apps/WinUI3Sample/MainWindow.xaml.cs
@@ -0,0 +1,12 @@
+using Microsoft.UI.Xaml;
+
+namespace WinUI3Sample;
+
+public sealed partial class MainWindow : Window
+{
+ public MainWindow()
+ {
+ InitializeComponent();
+ Title = "LVT WinUI3 Sample";
+ }
+}
diff --git a/tests/apps/WinUI3Sample/WinUI3Sample.csproj b/tests/apps/WinUI3Sample/WinUI3Sample.csproj
new file mode 100644
index 0000000..ad74b48
--- /dev/null
+++ b/tests/apps/WinUI3Sample/WinUI3Sample.csproj
@@ -0,0 +1,23 @@
+
+
+ WinExe
+ net10.0-windows10.0.19041.0
+ 10.0.17763.0
+ WinUI3Sample
+ WinUI3Sample
+ enable
+ true
+ None
+ true
+ x64
+ x64
+ win-x64
+ win-x64
+ true
+ true
+
+
+
+
+
+
diff --git a/tests/integration_tests.cpp b/tests/integration_tests.cpp
index 97298d7..c2193e4 100644
--- a/tests/integration_tests.cpp
+++ b/tests/integration_tests.cpp
@@ -402,6 +402,47 @@ static bool frameworks_contain_wpf(const json& j) {
return false;
}
+static bool frameworks_contain_winui3(const json& j) {
+ if (!j.contains("frameworks") || !j["frameworks"].is_array())
+ return false;
+ for (auto& fw : j["frameworks"]) {
+ if (fw.is_string() && fw.get().starts_with("winui3"))
+ return true;
+ }
+ return false;
+}
+
+static bool has_winui3_descendant(const json& el) {
+ if (el.value("framework", "") == "winui3")
+ return true;
+
+ if (el.contains("children") && el["children"].is_array()) {
+ for (auto& child : el["children"]) {
+ if (has_winui3_descendant(child))
+ return true;
+ }
+ }
+ return false;
+}
+
+static bool has_winui3_stitched_under_bridge(const json& el) {
+ if (el.value("className", "") == "Microsoft.UI.Content.DesktopChildSiteBridge" &&
+ el.contains("children") && el["children"].is_array()) {
+ for (auto& child : el["children"]) {
+ if (has_winui3_descendant(child))
+ return true;
+ }
+ }
+
+ if (el.contains("children") && el["children"].is_array()) {
+ for (auto& child : el["children"]) {
+ if (has_winui3_stitched_under_bridge(child))
+ return true;
+ }
+ }
+ return false;
+}
+
static const json* find_element_by_type_property(const json& el,
const std::string& type,
const std::string& property,
@@ -539,6 +580,129 @@ TEST_F(WpfSampleFixture, DurableKeyContract) {
EXPECT_EQ(queried.value("framework", ""), "wpf");
EXPECT_EQ(queried.value("name", ""), "OkButton");
}
+
+class WinUI3SampleFixture : public ::testing::Test {
+protected:
+ static void SetUpTestSuite() {
+ s_sample_exe = WINUI3_SAMPLE_EXE_PATH;
+ if (!fs::exists(s_sample_exe)) {
+ s_skip_reason = "WinUI3 sample app not found: " + s_sample_exe;
+ return;
+ }
+
+ STARTUPINFOA si = {sizeof(si)};
+ s_pi = {};
+ auto workdir = fs::path(s_sample_exe).parent_path().string();
+ std::string cmd = "\"" + s_sample_exe + "\"";
+ if (!CreateProcessA(nullptr, cmd.data(), nullptr, nullptr, FALSE, 0,
+ nullptr, workdir.c_str(), &si, &s_pi)) {
+ s_skip_reason = "Failed to launch WinUI3 sample app";
+ return;
+ }
+ s_pid = s_pi.dwProcessId;
+ if (s_pi.hProcess) {
+ WaitForInputIdle(s_pi.hProcess, 10000);
+ }
+
+ auto lvt = get_lvt_path();
+ for (int attempt = 0; attempt < 60; ++attempt) {
+ if (WaitForSingleObject(s_pi.hProcess, 0) == WAIT_OBJECT_0) {
+ s_skip_reason = "WinUI3 sample app exited before it became ready";
+ return;
+ }
+
+ auto output = run_command(make_cmd(lvt, get_pid_arg()));
+ auto j = json::parse(output, nullptr, false);
+ if (!j.is_discarded() && frameworks_contain_winui3(j) &&
+ j.contains("root") &&
+ json_tree_has_named_control(j["root"], "PrimaryButton") &&
+ json_tree_has_named_control(j["root"], "InputBox") &&
+ json_tree_has_named_control(j["root"], "ReadyCheckBox")) {
+ s_ready = true;
+ return;
+ }
+ Sleep(1000);
+ }
+
+ s_skip_reason = "WinUI3 sample app never became ready with WinUI3 framework and named controls";
+ }
+
+ static void TearDownTestSuite() {
+ if (s_pi.hProcess) {
+ TerminateProcess(s_pi.hProcess, 0);
+ CloseHandle(s_pi.hProcess);
+ CloseHandle(s_pi.hThread);
+ }
+ }
+
+ static void SkipIfNotReady() {
+ if (!s_ready)
+ GTEST_SKIP() << s_skip_reason;
+ }
+
+ static std::string get_pid_arg() {
+ return "--pid " + std::to_string(s_pid);
+ }
+
+ static PROCESS_INFORMATION s_pi;
+ static DWORD s_pid;
+ static bool s_ready;
+ static std::string s_sample_exe;
+ static std::string s_skip_reason;
+};
+
+PROCESS_INFORMATION WinUI3SampleFixture::s_pi = {};
+DWORD WinUI3SampleFixture::s_pid = 0;
+bool WinUI3SampleFixture::s_ready = false;
+std::string WinUI3SampleFixture::s_sample_exe;
+std::string WinUI3SampleFixture::s_skip_reason;
+
+TEST_F(WinUI3SampleFixture, DurableKeysDeterministicAndQueryable) {
+ SkipIfNotReady();
+
+ auto lvt = get_lvt_path();
+ auto first = run_command(make_cmd(lvt, get_pid_arg()));
+ auto second = run_command(make_cmd(lvt, get_pid_arg()));
+ ASSERT_FALSE(first.empty());
+ ASSERT_FALSE(second.empty());
+
+ auto j1 = json::parse(first, nullptr, false);
+ auto j2 = json::parse(second, nullptr, false);
+ ASSERT_FALSE(j1.is_discarded());
+ ASSERT_FALSE(j2.is_discarded());
+ ASSERT_TRUE(frameworks_contain_winui3(j1));
+ ASSERT_TRUE(has_winui3_stitched_under_bridge(j1["root"]))
+ << "WinUI3 elements were not grafted under DesktopChildSiteBridge";
+
+ std::vector elements;
+ collect_json_elements(j1["root"], elements);
+ ASSERT_GT(elements.size(), 0u);
+
+ std::set keys;
+ for (auto* el : elements) {
+ auto key = el->value("key", "");
+ EXPECT_FALSE(key.empty()) << "Element " << el->value("id", "?") << " has empty durable key";
+ EXPECT_TRUE(keys.insert(key).second) << "Duplicate durable key: " << key;
+ }
+
+ std::map firstMap;
+ std::map secondMap;
+ collect_key_contract_map(j1["root"], "0", firstMap);
+ collect_key_contract_map(j2["root"], "0", secondMap);
+ EXPECT_EQ(firstMap, secondMap);
+
+ auto* button = find_named_control(j1["root"], "PrimaryButton");
+ ASSERT_NE(button, nullptr);
+ EXPECT_EQ(button->value("framework", ""), "winui3");
+ EXPECT_EQ(button->value("type", ""), "Button");
+
+ auto buttonKey = button->value("key", "");
+ ASSERT_FALSE(buttonKey.empty());
+ auto byKey = run_command(make_cmd(lvt, get_pid_arg() + " --query " +
+ cmd_escape_arg(buttonKey) + " name"));
+ EXPECT_EQ(trim_crlf(byKey), "PrimaryButton");
+}
+
TEST_F(NotepadFixture, Win32BoundsReasonable) {
// Every element in the Win32 tree should have reasonable (non-extreme) bounds
auto lvt = get_lvt_path();