Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,9 @@ add_custom_command(TARGET lvt_wpf_tap POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${CMAKE_SOURCE_DIR}/src/tap_wpf/bin/Release/LvtWpfTap.dll"
"$<TARGET_FILE_DIR:lvt>/LvtWpfTap.dll"
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${CMAKE_SOURCE_DIR}/src/tap_wpf/LvtWpfTap.runtimeconfig.json"
"$<TARGET_FILE_DIR:lvt>/LvtWpfTap.runtimeconfig.json"
COMMENT "Copying managed WPF tree walker assembly"
)

Expand Down Expand Up @@ -288,19 +291,35 @@ target_link_libraries(lvt_unit_tests PRIVATE
)
add_test(NAME unit_tests COMMAND lvt_unit_tests)

# WPF sample app used by integration tests
set(WPF_SAMPLE_PROJECT "${CMAKE_SOURCE_DIR}/tests/apps/WpfSample/WpfSample.csproj")
set(WPF_SAMPLE_OUTPUT_DIR "${CMAKE_BINARY_DIR}/tests/apps/WpfSample")
add_custom_target(wpf_sample_app ALL
COMMAND dotnet build "${WPF_SAMPLE_PROJECT}" -c Release -o "${WPF_SAMPLE_OUTPUT_DIR}"
BYPRODUCTS "${WPF_SAMPLE_OUTPUT_DIR}/WpfSample.exe"
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
COMMENT "Building WPF sample app"
)
file(TO_NATIVE_PATH "${WPF_SAMPLE_OUTPUT_DIR}/WpfSample.exe" WPF_SAMPLE_EXE_NATIVE)
string(REPLACE "\\" "\\\\" WPF_SAMPLE_EXE_ESCAPED "${WPF_SAMPLE_EXE_NATIVE}")

# Integration tests — require a running Notepad instance
add_executable(lvt_integration_tests
tests/integration_tests.cpp
tests/lvt_integration_tests.rc
)
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)
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}"
)
target_link_libraries(lvt_integration_tests PRIVATE
GTest::gtest GTest::gtest_main
WIL::WIL nlohmann_json::nlohmann_json
comctl32
)
target_link_options(lvt_integration_tests PRIVATE "/MANIFEST:NO")
add_dependencies(lvt_integration_tests wpf_sample_app)
add_test(NAME integration_tests COMMAND lvt_integration_tests)

# Chromium plugin tests — DOM JSON format and native messaging protocol
Expand Down
9 changes: 9 additions & 0 deletions src/tap_wpf/LvtWpfTap.runtimeconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"runtimeOptions": {
"framework": {
"name": "Microsoft.WindowsDesktop.App",
"version": "8.0.0",
"rollForward": "LatestMajor"
}
}
}
3 changes: 2 additions & 1 deletion src/tap_wpf/wpf_tap_native.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,8 @@ static bool TryNetCore(const std::wstring& assemblyPath, const std::wstring& pip
fprintf(f, "{\n \"runtimeOptions\": {\n"
" \"framework\": {\n"
" \"name\": \"Microsoft.WindowsDesktop.App\",\n"
" \"version\": \"8.0.0\"\n"
" \"version\": \"8.0.0\",\n"
" \"rollForward\": \"LatestMajor\"\n"
" }\n }\n}\n");
fclose(f);
LogMsg("Created runtimeconfig.json");
Expand Down
6 changes: 6 additions & 0 deletions tests/apps/WpfSample/App.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<Application x:Class="WpfSample.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="MainWindow.xaml">
<Application.Resources />
</Application>
7 changes: 7 additions & 0 deletions tests/apps/WpfSample/App.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
using System.Windows;

namespace WpfSample;

public partial class App : Application
{
}
28 changes: 28 additions & 0 deletions tests/apps/WpfSample/MainWindow.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<Window x:Class="WpfSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="LVT WPF Sample" Width="420" Height="260"
WindowStartupLocation="CenterScreen">
<Grid Margin="16">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>

<TextBlock Text="Name:" VerticalAlignment="Center" Margin="0,0,8,8" />
<TextBox x:Name="NameBox" Grid.Column="1" Text="Ada" Margin="0,0,0,8" />
<CheckBox x:Name="AgreeCheck" Grid.Row="1" Grid.ColumnSpan="2"
Content="I agree" IsChecked="True" Margin="0,0,0,8" />
<StackPanel Grid.Row="2" Grid.ColumnSpan="2" Orientation="Horizontal" HorizontalAlignment="Right">
<TextBlock Text="Ready" VerticalAlignment="Center" Margin="0,0,12,0" />
<Button x:Name="CancelButton" Content="Cancel" Width="80" Margin="0,0,8,0" />
<Button x:Name="OkButton" Content="OK" Width="80" IsDefault="True" />
</StackPanel>
</Grid>
</Window>
11 changes: 11 additions & 0 deletions tests/apps/WpfSample/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System.Windows;

namespace WpfSample;

public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
10 changes: 10 additions & 0 deletions tests/apps/WpfSample/WpfSample.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
<AssemblyName>WpfSample</AssemblyName>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
157 changes: 157 additions & 0 deletions tests/integration_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include <array>
#include <map>
#include <set>
#include <vector>
#include <filesystem>
#include <fstream>
#include <nlohmann/json.hpp>
Expand Down Expand Up @@ -363,6 +364,44 @@ static void collect_key_contract_map(const json& el, const std::string& path,
}
}

static bool json_tree_has_named_control(const json& el, const std::string& name) {
if (el.contains("properties") && el["properties"].is_object() &&
el["properties"].value("name", "") == name) {
return true;
}
if (el.contains("children") && el["children"].is_array()) {
for (auto& child : el["children"]) {
if (json_tree_has_named_control(child, name))
return true;
}
}
return false;
}

static const json* find_named_control(const json& el, const std::string& name) {
if (el.contains("properties") && el["properties"].is_object() &&
el["properties"].value("name", "") == name) {
return &el;
}
if (el.contains("children") && el["children"].is_array()) {
for (auto& child : el["children"]) {
if (auto* found = find_named_control(child, name))
return found;
}
}
return nullptr;
}

static bool frameworks_contain_wpf(const json& j) {
if (!j.contains("frameworks") || !j["frameworks"].is_array())
return false;
for (auto& fw : j["frameworks"]) {
if (fw.is_string() && fw.get<std::string>().starts_with("wpf"))
return true;
}
return false;
}

static const json* find_element_by_type_property(const json& el,
const std::string& type,
const std::string& property,
Expand All @@ -382,6 +421,124 @@ static const json* find_element_by_type_property(const json& el,
return nullptr;
}

class WpfSampleFixture : public ::testing::Test {
protected:
static void SetUpTestSuite() {
s_sample_exe = WPF_SAMPLE_EXE_PATH;
if (!fs::exists(s_sample_exe)) {
s_skip_reason = "WPF 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 WPF sample app";
return;
}
s_pid = s_pi.dwProcessId;
if (s_pi.hProcess) {
WaitForInputIdle(s_pi.hProcess, 5000);
}

auto lvt = get_lvt_path();
for (int attempt = 0; attempt < 30; ++attempt) {
auto output = run_command(make_cmd(lvt, get_pid_arg()));
auto j = json::parse(output, nullptr, false);
if (!j.is_discarded() && frameworks_contain_wpf(j) &&
j.contains("root") &&
json_tree_has_named_control(j["root"], "OkButton") &&
json_tree_has_named_control(j["root"], "NameBox") &&
json_tree_has_named_control(j["root"], "AgreeCheck")) {
s_ready = true;
return;
}
Sleep(1000);
}

s_skip_reason = "WPF sample app never became ready with WPF 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 WpfSampleFixture::s_pi = {};
DWORD WpfSampleFixture::s_pid = 0;
bool WpfSampleFixture::s_ready = false;
std::string WpfSampleFixture::s_sample_exe;
std::string WpfSampleFixture::s_skip_reason;

TEST_F(WpfSampleFixture, DurableKeyContract) {
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_wpf(j1));

std::vector<const json*> elements;
collect_json_elements(j1["root"], elements);
ASSERT_GT(elements.size(), 0u);

std::set<std::string> 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<std::string, std::string> firstMap;
std::map<std::string, std::string> secondMap;
collect_key_contract_map(j1["root"], "0", firstMap);
collect_key_contract_map(j2["root"], "0", secondMap);
EXPECT_EQ(firstMap, secondMap);

auto* okButton = find_named_control(j1["root"], "OkButton");
ASSERT_NE(okButton, nullptr);
EXPECT_EQ(okButton->value("framework", ""), "wpf");
EXPECT_EQ(okButton->value("type", ""), "Button");

auto okKey = okButton->value("key", "");
ASSERT_FALSE(okKey.empty());
auto queryOutput = run_command(make_cmd(lvt, get_pid_arg() + " --query " + cmd_escape_arg(okKey)));
auto queried = json::parse(queryOutput, nullptr, false);
ASSERT_FALSE(queried.is_discarded()) << queryOutput;
EXPECT_EQ(queried.value("key", ""), okKey);
EXPECT_EQ(queried.value("type", ""), "Button");
EXPECT_EQ(queried.value("framework", ""), "wpf");
EXPECT_EQ(queried.value("name", ""), "OkButton");
}
TEST_F(NotepadFixture, Win32BoundsReasonable) {
// Every element in the Win32 tree should have reasonable (non-extreme) bounds
auto lvt = get_lvt_path();
Expand Down
Loading