202 lines
12 KiB
C++
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;
|
|
}
|