// SPDX-License-Identifier: LGPL-2.1-or-later #include #include #include #include #include #include #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(7,wp-1); std::copy_n(text,copied,reinterpret_cast(lp)); reinterpret_cast(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 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(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(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(SendMessageW(control,EM_GETFIRSTVISIBLELINE,0,0)),GetScrollPos(control,SB_HORZ), static_cast(SendMessageW(control,EM_GETLINECOUNT,0,0)),static_cast(GetWindowLongPtrW(control,GWL_STYLE))); RECT format{}; SendMessageW(control,EM_GETRECT,0,reinterpret_cast(&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(length), static_cast(SendMessageW(control,EM_POSFROMCHAR,0,0)),static_cast(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(range.utf16_start),static_cast(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; }