Files
wine-access/tests/review_capture_test.cpp
2026-10-11 22:07:14 -07:00

202 lines
12 KiB
C++

// SPDX-License-Identifier: LGPL-2.1-or-later
#include <windows.h>
#include <oleacc.h>
#include <cstdio>
#include <algorithm>
#include <string>
#include <vector>
#if __has_include("../review_capture.h")
#include "../review_capture.h"
#define HAS_CAPTURE 1
#endif
static LRESULT CALLBACK padded_name_proc(HWND window,UINT message,WPARAM wp,LPARAM lp) {
const WCHAR text[]={L'R',L'e',L'a',L'd',L'y',0,0xd800,0};
if(message==WM_GETTEXTLENGTH)return 7;
if(message==WM_GETTEXT && wp) {
const size_t copied=std::min<size_t>(7,wp-1);
std::copy_n(text,copied,reinterpret_cast<WCHAR*>(lp));
reinterpret_cast<WCHAR*>(lp)[copied]=0;
return copied;
}
return DefWindowProcW(window,message,wp,lp);
}
static bool check(bool value, const char *message) {
if (!value) std::fprintf(stderr, "FAIL: %s\n", message);
return value;
}
int main(int argc, char **argv) {
if (argc != 3 || std::string(argv[1]) != "--fixture") return 2;
std::string dos = argv[2];
if (dos.starts_with('/')) dos = "Z:" + dos;
std::replace(dos.begin(), dos.end(), '/', '\\');
std::string quoted = "\"" + dos + "\"";
int units = MultiByteToWideChar(CP_UTF8, 0, quoted.c_str(), -1, nullptr, 0);
std::vector<WCHAR> command(units);
MultiByteToWideChar(CP_UTF8, 0, quoted.c_str(), -1, command.data(), units);
STARTUPINFOW startup{}; startup.cb = sizeof(startup);
PROCESS_INFORMATION process{};
if (!CreateProcessW(nullptr, command.data(), nullptr, nullptr, FALSE, 0,
nullptr, nullptr, &startup, &process)) {
std::fprintf(stderr, "FAIL: fixture starts (%s, Windows error %lu)\n", argv[2], static_cast<unsigned long>(GetLastError()));
return 2;
}
HWND window = nullptr;
ULONGLONG deadline = GetTickCount64() + 10000;
while (!(window = FindWindowW(L"WineAccessReviewFixture", nullptr)) && GetTickCount64() < deadline) Sleep(20);
bool ok = check(window != nullptr, "fixture becomes ready");
if (window) {
Sleep(100);
#ifndef HAS_CAPTURE
ok = check(false, "review capture unavailable");
#else
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
std::fprintf(stderr, "capture test: COM ready\n");
ReviewIdentityStore store;
auto capture = [&]() {
auto focus = store.observe_foreground();
std::fprintf(stderr, "capture test: focus %s\n", focus.focus.c_str());
return capture_review_window(store, focus, GetTickCount64() + 500);
};
auto joined = [](const ReviewCapture &data) {
std::string all;
for (const auto &node : data.nodes) {
all += node.name;
for (const auto &range : node.ranges) all += range.text + "\n";
}
return all;
};
auto data = capture();
std::fprintf(stderr, "capture test: initial %s\n", data.status.c_str());
ok &= check(data.status == "ok", "initial capture succeeds");
const auto text = joined(data);
ok &= check(text.find("alpha beta") != std::string::npos, "visible Edit text");
ok &= check(text.find("output second") != std::string::npos, "read-only output text");
ok &= check(text.find("Open & close") != std::string::npos, "caption mnemonics and literal ampersand");
ok &= check(text.find("native & item") != std::string::npos, "native MSAA list items retain literal ampersands");
ok &= check(text.find("private-review-secret") == std::string::npos && text.find("hidden-review-secret") == std::string::npos,
"password and hidden contents excluded");
ok &= check(SendMessageW(window, WM_APP + 11, 0, 0), "review preserves focus and selection");
for (int mode : {1, 2, 3, 4, 5, 6, 7, 8, 9, 14, 15, 17, 18, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33}) {
std::fprintf(stderr, "capture test: mode %d\n", mode);
SendMessageW(window, WM_APP + 10, mode, 0);
auto started=GetTickCount64();
auto result = capture();
std::fprintf(stderr,"capture test: status=%s elapsed=%llu ms\n",result.status.c_str(),static_cast<unsigned long long>(GetTickCount64()-started));
const auto value = joined(result);
if (mode == 7) ok &= check(result.status == "limited" && result.nodes.empty(), "source limit rejects partial capture");
else if (mode == 9) ok &= check(result.status == "stale" && result.nodes.empty(), "same-length update during capture rejects mixed data");
else if (mode == 14 || mode == 15 || mode == 18 || mode == 23) ok &= check(result.status == "stale" && result.nodes.empty(), "protection/list/viewport transition rejects old capture");
else if (mode == 21) ok &= check(result.status != "ok" && result.nodes.empty(), "delayed native call respects capture deadline");
else if (mode == 27) ok &= check(result.status != "ok" && result.nodes.empty() && GetTickCount64()-started<600,"delayed scalar batch respects capture deadline");
else ok &= check(result.status == "ok", "configured capture succeeds");
ok &= check(value.find("private-review-secret") == std::string::npos && value.find("hidden-review-secret") == std::string::npos,
"all captures exclude protected/hidden text");
if (mode == 1) {
bool found = false;
for (const auto &node : result.nodes) for (const auto &range : node.ranges)
if (range.text == "beta") {
found = range.start == 8 && range.end == 12 && range.utf16_start == 9 && range.utf16_end == 13;
}
ok &= check(found, "surrogate before viewport retains absolute rune offsets");
}
if (mode == 2) {
int wrapped = 0; bool blank = false;
for (const auto &node : result.nodes) for (const auto &range : node.ranges) {
if (range.text.find("wrapped") != std::string::npos) ++wrapped;
if (range.text == "\n") blank = true;
}
ok &= check(wrapped > 1 && blank, "visual wraps and blank lines");
}
if (mode == 3) ok &= check(value.find("output second") != std::string::npos && !result.source.focus.empty(), "empty command remains review anchor");
if (mode == 5) ok &= check(value.find("alpha beta") == std::string::npos, "partly clipped text is excluded");
if (mode == 6) ok &= check(value.find("omega zeta") != std::string::npos && value.find("alpha beta") == std::string::npos, "same-length replacement refreshes text");
if (mode == 8) ok &= check(result.source.bounds.left < 0, "negative screen coordinates remain valid");
if (mode == 14) ok &= check(SendMessageW(window,WM_APP + 14,0,0), "password transition is excluded before copying text");
if (mode == 17) ok &= check(value.find("table & item") != std::string::npos && value.find("ready") != std::string::npos, "visible native ListView cells retain text and ampersands");
if (mode == 25) ok &= check(value.find("literal & label") != std::string::npos, "SS_NOPREFIX captions retain literal ampersands");
if (mode == 26) {
size_t visible_lines=0;
for(const auto &node:result.nodes)for(const auto &range:node.ranges)
if(range.text=="stable buffered output line")++visible_lines;
ok &= check(visible_lines>=15,"frame-pumped multiline output captures within the deadline");
}
if(mode==27){
Sleep(750);SendMessageW(window,WM_APP+10,0,0);
auto recovered=capture();
ok &= check(recovered.status=="ok" && joined(recovered).find("alpha beta")!=std::string::npos,"late scalar callbacks preserve storage and allow a fresh capture");
}
if(mode==28)ok &= check(value.find("top clipped visible line")!=std::string::npos,"top-clipped Edit captures its lower visible rows");
if(mode==29)ok &= check(value.find("nested output ready")!=std::string::npos && SendMessageW(window,WM_APP+16,0,0),"native containers do not synthesize duplicate accessibility trees");
if(mode==30)ok &= check(value.find("active mdi text")!=std::string::npos && value.find("covered mdi text")==std::string::npos,"maximized MDI child excludes covered background text");
if(mode==31)ok &= check(value.find("active mdi text")!=std::string::npos && value.find("covered mdi text")!=std::string::npos,"side-by-side MDI children retain both visible documents");
if (mode == 31) ok &= check(value.find("Active") != std::string::npos && value.find("Background") != std::string::npos,
"side-by-side MDI window titles remain available for review");
if (mode == 32) ok &= check(value.find("deep clipped visible line") != std::string::npos,
"hundreds of clipped rows do not hide the visible tail");
if (mode == 33) {
bool safe=true, status_present=false;
const auto status_id=store.identity(GetDlgItem(window,114))+"_a";
for(const auto &node:result.nodes) {
if(node.id==status_id)status_present=true;
safe &= node.name.find('\0')==std::string::npos;
for(const auto &range:node.ranges)safe &= range.text.find('\0')==std::string::npos;
}
ok &= check(status_present && safe,"padded MSAA status name produces D-Bus-safe accessibility text");
ok &= check(value.find("Ready")!=std::string::npos,
"status bar retains its exposed label");
}
if (mode == 21) {
Sleep(750);
ok &= check(SendMessageW(window,WM_APP + 15,0,0), "remote buffer lives until the delayed target returns");
}
if (mode == 22 || mode == 24) {
bool visible=false;
auto field=store.identity(GetDlgItem(window,mode == 22 ? 109 : 110));
for (const auto &node:result.nodes) if (node.id == field) for (const auto &range:node.ranges)
if (range.utf16_start > 0 && !range.text.empty()) visible=true;
if (!visible) {
HWND control=GetDlgItem(window,mode == 22 ? 109 : 110);
std::fprintf(stderr,"scroll diagnostic first=%lld scroll=%d lines=%lld style=%llx\n",
static_cast<long long>(SendMessageW(control,EM_GETFIRSTVISIBLELINE,0,0)),GetScrollPos(control,SB_HORZ),
static_cast<long long>(SendMessageW(control,EM_GETLINECOUNT,0,0)),static_cast<unsigned long long>(GetWindowLongPtrW(control,GWL_STYLE)));
RECT format{}; SendMessageW(control,EM_GETRECT,0,reinterpret_cast<LPARAM>(&format));
auto length=SendMessageW(control,WM_GETTEXTLENGTH,0,0);
std::fprintf(stderr,"scroll length=%lld positions=%llx/%llx format=%d,%d,%d,%d\n",static_cast<long long>(length),
static_cast<unsigned long long>(SendMessageW(control,EM_POSFROMCHAR,0,0)),static_cast<unsigned long long>(SendMessageW(control,EM_POSFROMCHAR,length-1,0)),
format.left,format.top,format.right,format.bottom);
for (const auto &node:result.nodes) if (node.id == field) for (const auto &range:node.ranges)
std::fprintf(stderr,"scroll range %llu %llu %s\n",static_cast<unsigned long long>(range.utf16_start),static_cast<unsigned long long>(range.utf16_end),range.text.c_str());
}
ok &= check(visible, "horizontally scrolled Edit retains visible text and absolute offsets");
}
if (mode != 4) ok &= check(SendMessageW(window, WM_APP + 11, 0, 0), "capture leaves caret and focus unchanged");
}
// A local provider avoids cross-process WM_GETTEXT marshalling, which
// itself truncates at the first NUL. Its BSTR retains malformed padding.
WNDCLASSW local{};local.hInstance=GetModuleHandleW(nullptr);
local.lpfnWndProc=DefWindowProcW;local.lpszClassName=L"WineReviewLocalRoot";
RegisterClassW(&local);
HWND local_root=CreateWindowW(local.lpszClassName,L"Padded MSAA name",WS_OVERLAPPEDWINDOW,
100,100,400,200,nullptr,nullptr,local.hInstance,nullptr);
local.lpfnWndProc=padded_name_proc;local.lpszClassName=L"WineReviewPaddedName";
RegisterClassW(&local);
HWND padded=CreateWindowW(local.lpszClassName,L"",WS_CHILD|WS_VISIBLE,
10,10,200,30,local_root,nullptr,local.hInstance,nullptr);
ShowWindow(local_root,SW_SHOW);SetForegroundWindow(local_root);SetFocus(padded);
auto padded_focus=store.observe_foreground();
auto padded_capture=capture_review_window(store,padded_focus,GetTickCount64()+500);
ok &= check(padded_capture.status=="ok" && joined(padded_capture).find("Ready")!=std::string::npos,
"malformed UTF-16 padding does not discard the valid MSAA label");
DestroyWindow(local_root);
CoUninitialize();
#endif
PostMessageW(window, WM_CLOSE, 0, 0);
}
if (WaitForSingleObject(process.hProcess, 3000) != WAIT_OBJECT_0) TerminateProcess(process.hProcess, 2);
CloseHandle(process.hThread); CloseHandle(process.hProcess);
if (ok) std::puts("PASS: native visible Wine review capture and independent caret");
return ok ? 0 : 1;
}