#include "app.h" #include "diagnostics.h" #include #include #include #include namespace ks { struct InputRegressionAccess { static bool deferring(App &app) { std::lock_guard lock(app.stateMutex); return app.typingTarget != nullptr; } }; } namespace { ks::App *app{}; HWND host{}, field{}; ks::Clip saved; bool captured = false, failed = false; bool conversionOnly = false; int pasteBehavior = 0; int enterPresses = 0; int copyFailuresRemaining = 0, copyRequests = 0; int droppedSelectionKeys = 0; bool temporaryClipboardExcluded = true; bool clipboardAccessTimedOut = false; LRESULT CALLBACK observeEnter(HWND w, UINT message, WPARAM wp, LPARAM lp, UINT_PTR, DWORD_PTR) { if (message == WM_KEYDOWN && wp == VK_LEFT && droppedSelectionKeys > 0 && (GetKeyState(VK_SHIFT) & 0x8000)) { --droppedSelectionKeys; return 0; } // This bare EDIT control is our test fixture, not a full editor with // clipboard retries. Keep clipboard access on its owning UI thread while // it handles Ctrl+C/Ctrl+V, so unrelated Windows clipboard listeners cannot // make the fixture silently discard the operation under test. struct ClipboardAccess { bool opened = false; ~ClipboardAccess() { if (opened) CloseClipboard(); } } access; if (message == WM_CHAR && (wp == 3 || wp == 22)) { auto deadline = GetTickCount64() + 1000; do { if (OpenClipboard(w)) { access.opened = true; break; } Sleep(10); } while (GetTickCount64() < deadline); if (!access.opened) { clipboardAccessTimedOut = true; return 0; } } if ((message == WM_CHAR && wp == 3) || message == WM_PASTE) { temporaryClipboardExcluded &= IsClipboardFormatAvailable( RegisterClipboardFormatW(L"ExcludeClipboardContentFromMonitorProcessing")) != FALSE; if (message == WM_CHAR) { ++copyRequests; // Native EDIT silently ignores Ctrl+C when OpenClipboard fails. // Reproduce that outcome deterministically, without depending on // the timing of the Windows clipboard-history service. if (copyFailuresRemaining > 0) { --copyFailuresRemaining; return 0; } } } if (message == WM_PASTE && pasteBehavior) { if (pasteBehavior == 2) SendMessageW(w, EM_REPLACESEL, TRUE, (LPARAM)L"other!"); return 0; } if (message == WM_KEYDOWN && wp == VK_RETURN) ++enterPresses; return DefSubclassProc(w, message, wp, lp); } int stage = 0, ru = -1, en = -1; size_t punctuationCase = 0; bool typeDuringCorrection = false, typedDuringCorrection = false; const std::vector> punctuationCases{ {L"(ljvf) ", L"(дома) "}, {L"[ljvf] ", L"[дома] "}, {L"{ljvf} ", L"{дома} "}, {L"ljvf: ", L"дома: "}, {L"ljvf; ", L"дома; "}, {L"#ljvf%; ", L"#дома%; "}}; ULONGLONG deadline{}; std::wstring sentinel = L"KSwitcher integration clipboard sentinel"; std::string diagnosticLog() { std::ifstream file(ks::dataDirectory() / L"kswitcher.log"); return {std::istreambuf_iterator(file), std::istreambuf_iterator()}; } size_t diagnosticStart{}; void require(bool ok, const char *name) { if (!ok || clipboardAccessTimedOut) { wchar_t observed[2048]{}; GetWindowTextW(field, observed, 2048); std::cerr << "Editor at failure: " << ks::utf8(observed) << "; foregroundSame=" << (GetForegroundWindow() == host) << "; inputVersion=" << app->inputVersion.load() << '\n'; if (clipboardAccessTimedOut) throw std::runtime_error("Test editor could not acquire clipboard within 1000 ms"); throw std::runtime_error(name); } std::cout << "PASS " << name << '\n'; } std::wstring text() { wchar_t buffer[2048]{}; GetWindowTextW(field, buffer, 2048); return buffer; } void prepare(const wchar_t *value, bool select) { app->resetInput(); SetWindowTextW(field, value); ks::activateWindow(host); SetFocus(field); int end = GetWindowTextLengthW(field); SendMessageW(field, EM_SETSEL, select ? 0 : end, end); if (GetForegroundWindow() != host) throw std::runtime_error("Test editor could not acquire foreground; no input was sent"); } void record(const std::wstring &keys) { std::lock_guard lock(app->stateMutex); for (wchar_t c : keys) app->run.record(c, host, app->layouts[en].handle, GetTickCount64()); } void finish() { KillTimer(host, 1); if (captured) { try { app->ownSequence = ks::writeClipboard(app->window, saved); } catch (...) { failed = true; } } DestroyWindow(host); PostMessageW(app->window, WM_CLOSE, 0, 0); } LRESULT CALLBACK procedure(HWND w, UINT msg, WPARAM wp, LPARAM lp) { // Model a host that does not forward layout requests to its embedded editor. if (msg == WM_INPUTLANGCHANGEREQUEST) return 0; if (msg == WM_TIMER) { // Input tests must not start a network update check. Its close veto can // otherwise leave a completed test running after the editor is gone. if (app->window) KillTimer(app->window, 5); try { if (GetTickCount64() > deadline) throw std::runtime_error("Native integration timed out"); if (app->busy) { if (typeDuringCorrection && ks::InputRegressionAccess::deferring(*app)) { typeDuringCorrection = false; typedDuringCorrection = true; std::vector inputs; for (auto [key, up] : std::vector>{ {'F', false}, {'F', true}, {VK_LSHIFT, false}, {'F', false}, {'F', true}, {VK_LSHIFT, true}, {'B', false}, {'B', true}, {VK_BACK, false}, {VK_BACK, true}}) { auto input = ks::keyboardInput(key, up ? KEYEVENTF_KEYUP : 0); input.ki.dwExtraInfo = 123; // External input must traverse the actual hook. inputs.push_back(input); } require(SendInput((UINT)inputs.size(), inputs.data(), sizeof(INPUT)) == inputs.size(), "concurrent typing submitted during selection/paste transaction"); } return 0; } switch (stage++) { case 0: saved = ks::readClipboard(app->window, 64 * 1024 * 1024); captured = true; app->ownSequence = ks::writeClipboard(app->window, ks::textClip(sentinel)); prepare(L"ghbdtn", true); app->transform(ks::Direct + ru); break; case 1: std::cout << "Observed editor: " << ks::utf8(text()) << "; busy=" << app->busy << "; foreground=" << (GetForegroundWindow() == host) << "\n"; require(text() == L"привет", "selection conversion through actual Ctrl+C / Ctrl+V"); require(ks::readClipboard(app->window).text() == sentinel, "clipboard restored"); app->transform(ks::Undo); break; case 2: require(text() == L"ghbdtn", "verified undo"); prepare(L"hello world", true); app->transform(ks::Case); break; case 3: require(text() == L"Hello World", "case conversion"); droppedSelectionKeys = 5; app->transform(ks::Case); break; case 4: require(text() == L"HELLO WORLD", "consecutive command reselects verified previous result"); require(droppedSelectionKeys == 0, "partial suffix selection recovered after dropped arrow keys"); prepare(L"OLDghbdtn", false); record(L"ghbdtn"); app->transform(ks::Direct + ru); break; case 5: std::cout << "Recent observed: " << ks::utf8(text()) << "\n"; require(text() == L"OLDпривет", "recent-input conversion preserves adjacent older text"); prepare(L"OLDabcdef", false); record(L"ghbdtn"); diagnosticStart = diagnosticLog().size(); app->busy = true; app->transform(ks::Direct + ru); app->transform(ks::Direct + ru); app->busy = false; app->transform(ks::Direct + ru); break; case 6: require(text() == L"OLDabcdef", "recent-input mismatch refuses replacement"); { auto diagnostics = diagnosticLog(); diagnostics = diagnostics.substr(std::min(diagnosticStart, diagnostics.size())); require(diagnostics.find("reason=busy") != std::string::npos, "busy conversion attempt is logged"); require(diagnostics.find("repeat=3") != std::string::npos, "three repeated conversion attempts are logged"); require(diagnostics.find("\"outcome\":\"recent-verification-mismatch\"") != std::string::npos, "failed recent-word verification has explicit outcome"); require(diagnostics.find("\"original\":\"abcdef\"") != std::string::npos && diagnostics.find("physicalKeys=\\\"ghbdtn\\\"") != std::string::npos, "conversion diagnostics contain actual text and physical input for reproduction"); } if (conversionOnly) { pasteBehavior = 1; prepare(L"ghbdtn", true); app->transform(ks::Direct + ru); stage = 40; break; } app->settings.snippets = {{L";addr", L"LINE1\r\n{cursor}LINE2"}}; prepare(L"prefix ;addr", false); record(L";addr"); app->transform(0, 0); break; case 7: { require(text() == L"prefix LINE1\r\nLINE2", "multiline snippet expansion"); DWORD start{}, end{}; SendMessageW(field, EM_GETSEL, (WPARAM)&start, (LPARAM)&end); require(start == end && start == 14, "verified snippet cursor placement"); require(ks::readClipboard(app->window).text() == sentinel, "clipboard restored after repeated copy transactions"); app->settings.autoCorrect = true; app->settings.switchLayout = true; app->settings.autoLanguages = {L"en", L"ru"}; app->settings.cycle = {app->layouts[en].id, app->layouts[ru].id}; ActivateKeyboardLayout(app->layouts[en].handle, 0); prepare(L"ghbdtn ", false); record(L"ghbdtn "); app->transform(ks::AutoCorrect); break; } case 8: require(text() == L"привет ", "automatic dictionary correction in native editor"); require(ks::windowLayout(host) == app->layouts[ru].handle, "auto-correction switches focused editor layout even when host ignores requests"); app->transform(ks::Undo); break; case 9: require(text() == L"ghbdtn ", "automatic correction undo retains space"); require(ks::windowLayout(host) == app->layouts[en].handle, "undo restores the original keyboard layout"); prepare(L"ghbdtn ", false); record(L"ghbdtn "); app->transform(ks::AutoCorrect); break; case 10: require(text() == L"ghbdtn ", "undo prevents repeated automatic correction"); prepare(L"hello ", false); record(L"hello "); app->transform(ks::AutoCorrect); break; case 11: require(text() == L"hello ", "known word stays unchanged in editor"); require(ks::readClipboard(app->window).text() == sentinel, "automatic correction restores clipboard"); app->autoCorrector = ks::AutoCorrector{}; prepare(L"OLDghbdtn ", false); record(L"ghbdtn "); app->transform(ks::AutoCorrect); break; case 12: require(text() == L"OLDghbdtn ", "automatic correction refuses partial word replacement"); DestroyWindow(field); field = CreateWindowW(L"EDIT", L"", WS_CHILD | WS_VISIBLE | WS_BORDER | ES_PASSWORD, 20, 20, 590, 30, host, nullptr, GetModuleHandleW(nullptr), nullptr); SetWindowSubclass(field, observeEnter, 1, 0); prepare(L"ghbdtn ", false); record(L"ghbdtn "); app->transform(ks::AutoCorrect); break; case 13: require(text() == L"ghbdtn ", "password input is never automatically corrected"); require(ks::readClipboard(app->window).text() == sentinel, "password input never copied to clipboard"); DestroyWindow(field); field = CreateWindowW(L"EDIT", L"", WS_CHILD | WS_VISIBLE | WS_BORDER, 20, 20, 590, 30, host, nullptr, GetModuleHandleW(nullptr), nullptr); SetWindowSubclass(field, observeEnter, 1, 0); prepare(L"(ljvf ", false); record(L"(ljvf "); app->transform(ks::AutoCorrect); break; case 14: require(text() == L"(дома ", "automatic correction preserves opening parenthesis in editor"); app->transform(ks::Undo); break; case 15: require(text() == L"(ljvf ", "undo preserves opening parenthesis in editor"); require(ks::readClipboard(app->window).text() == sentinel, "parenthesized correction restores clipboard"); app->settings.autoPrefixChars += L"#"; app->settings.autoSuffixChars += L"%"; app->autoCorrector = ks::AutoCorrector{}; prepare(punctuationCases[0].first.c_str(), false); record(punctuationCases[0].first); app->transform(ks::AutoCorrect); break; case 16: require(text() == punctuationCases[punctuationCase].second, "configured punctuation preserved in editor"); if (++punctuationCase < punctuationCases.size()) { ActivateKeyboardLayout(app->layouts[en].handle, 0); prepare(punctuationCases[punctuationCase].first.c_str(), false); record(punctuationCases[punctuationCase].first); app->transform(ks::AutoCorrect); stage = 16; } else app->transform(ks::Undo); break; case 17: require(text() == punctuationCases.back().first, "undo preserves custom punctuation"); stage = 20; break; case 40: { auto rows = ks::parseOperationLog(ks::wide(diagnosticLog())); require(text() == L"ghbdtn" && rows.back().outcome == L"Original text remained", "blocked paste records original text remaining in the actual editor"); require(ks::readClipboard(app->window).text() == sentinel, "readback preserves clipboard after blocked paste"); pasteBehavior = 2; prepare(L"ghbdtn", true); app->transform(ks::Direct + ru); break; } case 41: { auto rows = ks::parseOperationLog(ks::wide(diagnosticLog())); require(text() == L"other!" && rows.back().outcome == L"Readback differs", "unexpected editor content records readback mismatch"); pasteBehavior = 0; prepare(L"привет", true); app->transform(ks::Direct + ru); break; } case 42: { auto rows = ks::parseOperationLog(ks::wide(diagnosticLog())); require(text() == L"привет" && rows.back().outcome == L"Conversion unchanged" && rows.back().problem, "manual unchanged conversion is recorded as a problem"); diagnosticStart = diagnosticLog().size(); app->settings.writeLog = false; ks::setLoggingEnabled(false); prepare(L"ghbdtn", true); app->transform(ks::Direct + ru); break; } case 43: require(text() == L"привет" && diagnosticLog().size() == diagnosticStart, "disabled diagnostics preserves conversion and writes no records"); ks::setLoggingEnabled(true); prepare(L"PuntoSwitcher ", true); app->transform(ks::Direct + ru); break; case 44: app->transform(ks::Direct + en); break; case 45: require(text() == L"PuntoSwitcher ", "previous conversion restores CamelCase and trailing space"); droppedSelectionKeys = 5; app->transform(ks::Direct + ru); break; case 46: require(text() == ks::convertTo(L"PuntoSwitcher ", app->layouts, ru, en), "repeat conversion recovers Switcher suffix and preserves full result"); require(droppedSelectionKeys == 0, "CamelCase suffix regression dropped five selection keys"); require(ks::readClipboard(app->window).text() == sentinel, "suffix recovery restores original clipboard"); // Derive the actual text from the physical keys, preserving an older prefix. prepare((L"OLD" + ks::convertTo(L"snipping ", app->layouts, ru, en)).c_str(), false); record(L"snipping "); droppedSelectionKeys = 4; app->transform(ks::Direct + en); break; case 47: require(text() == L"OLDsnipping ", "recent Cyrillic suffix recovery preserves older prefix"); require(droppedSelectionKeys == 0, "recent suffix regression dropped four selection keys"); finish(); break; case 30: case 32: case 34: case 37: // Earlier password/word tests replace the original multiline // editor with a single-line EDIT. Restore the snippet fixture: // a single-line control cannot insert the tested Enter action. if (!(GetWindowLongPtrW(field, GWL_STYLE) & ES_MULTILINE)) { DestroyWindow(field); field = CreateWindowW(L"EDIT", L"", WS_CHILD | WS_VISIBLE | WS_BORDER | ES_MULTILINE | ES_WANTRETURN, 20, 20, 590, 230, host, nullptr, GetModuleHandleW(nullptr), nullptr); SetWindowSubclass(field, observeEnter, 1, 0); } if (!captured) { saved = ks::readClipboard(app->window, 64 * 1024 * 1024); captured = true; } app->ownSequence = ks::writeClipboard(app->window, ks::textClip(sentinel)); app->settings.autoCorrect = stage == 35; app->settings.autoLanguages = {L"en", L"ru"}; app->settings.cycle = {app->layouts[en].id, app->layouts[ru].id}; app->settings.switchLayout = false; app->autoCorrector = ks::AutoCorrector{}; app->autoPredictor = ks::AutoCorrector{}; app->autoPredictor.configure(L"dictionaries", app->settings.autoLanguages); app->settings.snippets = {{L";r", stage == 31 ? L"rider .{cursor}" : L"rider .{cursor}{enter}"}}; enterPresses = 0; copyRequests = 0; copyFailuresRemaining = stage == 38 ? 10 : stage == 33 ? 1 : 0; temporaryClipboardExcluded = true; ActivateKeyboardLayout(app->layouts[en].handle, 0); prepare(L"prefix ", false); { std::vector inputs; const std::vector keys = stage == 35 ? std::vector{VK_OEM_1, 'R', 'G', 'H', 'B', 'D', 'T', 'N', VK_SPACE} : std::vector{VK_OEM_1, 'R', VK_SPACE, 'X'}; for (WORD key : keys) for (DWORD flags : {DWORD(0), DWORD(KEYEVENTF_KEYUP)}) { auto input = ks::keyboardInput(key, flags); input.ki.dwExtraInfo = 123; inputs.push_back(input); } require(SendInput((UINT)inputs.size(), inputs.data(), sizeof(INPUT)) == inputs.size(), "snippet trigger and following text submitted without a pause"); } break; case 31: require(text() == L"prefix rider . x", "snippet expands through keyboard hook and preserves following typing"); require(ks::readClipboard(app->window).text() == sentinel, "hook snippet restores clipboard"); require(enterPresses == 0, "snippet without Enter marker sends no Enter"); stage = 32; break; case 33: require(text() == L"prefix rider .\r\n x", "Enter action precedes buffered typing"); require(copyRequests == 2 && copyFailuresRemaining == 0, "snippet retries a silently dropped copy without duplicating input"); require(temporaryClipboardExcluded, "temporary snippet clipboard contents are excluded from Windows history"); require(enterPresses == 1, "Enter marker sends an actual Enter key press"); require(ks::readClipboard(app->window).text() == sentinel, "Enter snippet restores clipboard"); stage = 34; break; case 35: require(text() == L"prefix rider .\r\nghbdtn ", "snippet replay does not trigger automatic correction"); require(app->settings.autoCorrect, "snippet preserves global auto-correction setting"); require(enterPresses == 1, "snippet punctuation and Enter survive with auto-correction enabled"); { std::vector inputs; for (WORD key : std::wstring(L"GHBDTN ")) for (DWORD flags : {DWORD(0), DWORD(KEYEVENTF_KEYUP)}) { auto input = ks::keyboardInput(key == L' ' ? VK_SPACE : key, flags); input.ki.dwExtraInfo = 123; inputs.push_back(input); } require(SendInput((UINT)inputs.size(), inputs.data(), sizeof(INPUT)) == inputs.size(), "normal typing submitted after snippet completes"); } break; case 36: require(text() == L"prefix rider .\r\nghbdtn привет ", "automatic correction resumes for subsequent normal typing"); stage = 37; break; case 38: require(copyRequests == 3, "copy retries are bounded to three attempts"); require(text() == L"prefix ;r x" && enterPresses == 0, "exhausted copy retries preserve trigger and following input without Enter"); require(ks::readClipboard(app->window).text() == sentinel, "exhausted copy retries restore the clipboard"); copyFailuresRemaining = 0; finish(); break; case 20: if (!captured) { saved = ks::readClipboard(app->window, 64 * 1024 * 1024); captured = true; app->ownSequence = ks::writeClipboard(app->window, ks::textClip(sentinel)); } app->settings.autoCorrect = true; app->settings.switchLayout = true; app->settings.autoLanguages = {L"en", L"ru"}; app->settings.cycle = {app->layouts[en].id, app->layouts[ru].id}; app->autoCorrector = ks::AutoCorrector{}; ActivateKeyboardLayout(app->layouts[en].handle, 0); prepare(L"ghbdtn ", false); record(L"ghbdtn "); typeDuringCorrection = true; app->transform(ks::AutoCorrect); stage = 18; break; case 18: require(typedDuringCorrection && text() == L"привет аА", "typing during correction replays in order with shift and backspace in new layout"); require(ks::readClipboard(app->window).text() == sentinel, "concurrent typing preserves clipboard restoration"); ActivateKeyboardLayout(app->layouts[en].handle, 0); prepare(L"OLDghbdtn ", false); record(L"ghbdtn "); typedDuringCorrection = false; typeDuringCorrection = true; app->transform(ks::AutoCorrect); break; case 19: require(typedDuringCorrection && text() == L"OLDghbdtn fF", "rejected correction collapses selection before replaying typing in original layout"); ActivateKeyboardLayout(app->layouts[en].handle, 0); prepare(L"", false); app->autoPredictor.configure(L"dictionaries", app->settings.autoLanguages); { std::vector inputs; for (WORD key : std::wstring(L"GHBDTN F")) for (DWORD flags : {DWORD(0), DWORD(KEYEVENTF_KEYUP)}) { auto input = ks::keyboardInput(key == L' ' ? VK_SPACE : key, flags); input.ki.dwExtraInfo = 123; inputs.push_back(input); } require(SendInput((UINT)inputs.size(), inputs.data(), sizeof(INPUT)) == inputs.size(), "continuous typing submitted in a single batch without a pause after Space"); } stage = 21; break; case 21: { require(text() == L"привет а", "automatic correction preserves immediate next-word typing without a pause"); auto uk = std::find_if(app->layouts.begin(), app->layouts.end(), [](const auto &layout) { return ks::layoutLanguage(layout) == L"uk"; }); require(uk != app->layouts.end(), "Ukrainian layout available for punctuation-key regression"); app->settings.autoLanguages = {L"en", L"uk"}; app->settings.cycle = {app->layouts[en].id, uk->id}; app->settings.autoWords[L"uk"] = {L"робить"}; app->autoCorrector = ks::AutoCorrector{}; ActivateKeyboardLayout(app->layouts[en].handle, 0); prepare(L"hj,bnm ", false); record(L"hj,bnm "); app->transform(ks::AutoCorrect); stage = 22; break; } case 22: require(text() == L"робить ", "automatic correction selects and converts the whole word across an internal comma"); stage = 30; break; } } catch (const std::exception &e) { failed = true; std::cerr << "FAIL " << e.what() << '\n'; finish(); } return 0; } if (msg == WM_CLOSE) { failed = true; finish(); return 0; } return DefWindowProcW(w, msg, wp, lp); } } // namespace int main(int argc, char **argv) { conversionOnly = argc > 1 && std::string(argv[1]) == "--conversion-only"; CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); auto root = std::filesystem::temp_directory_path() / (L"KSwitcher-integration-" + std::to_wstring(GetCurrentProcessId())); try { { ks::App instance(GetModuleHandleW(nullptr), false, root); app = &instance; app->settings.history = false; app->settings.switchLayout = false; if (argc > 1 && std::string(argv[1]) == "--logs-only") { ks::showLogs(instance); HWND viewer = instance.logWindow; HWND rows = GetDlgItem(viewer, 602); if (argc > 2 && std::string(argv[2]) == "--preview") { MSG message{}; while (IsWindow(viewer) && GetMessageW(&message, nullptr, 0, 0) > 0) { TranslateMessage(&message); DispatchMessageW(&message); } CoUninitialize(); return 0; } require(rows && (GetWindowLongPtrW(rows, GWL_STYLE) & LVS_TYPEMASK) == LVS_REPORT, "log viewer uses report ListView"); require(Header_GetItemCount(ListView_GetHeader(rows)) == 6, "log viewer has six sortable columns"); require(GetDlgItem(viewer, 607) && GetDlgItem(viewer, 608) && GetDlgItem(viewer, 609) && GetDlgItem(viewer, 610) && GetDlgItem(viewer, 611), "log viewer exposes program, command, result, problems filter and detail panel"); require(GetDlgItem(viewer, IDCANCEL) != rows, "log rows and Close have different IDs"); SetFocus(rows); SendMessageW(viewer, WM_COMMAND, MAKEWPARAM(IDCANCEL, EN_SETFOCUS), (LPARAM)rows); NMHDR clicked{rows, 602, NM_CLICK}; SendMessageW(viewer, WM_NOTIFY, 602, (LPARAM)&clicked); require(IsWindow(viewer), "focus and click notifications do not close log viewer"); ks::showLogs(instance); require(instance.logWindow == viewer, "opening logs reuses the existing window"); SendMessageW(viewer, WM_COMMAND, MAKEWPARAM(603, BN_CLICKED), (LPARAM)GetDlgItem(viewer, 603)); require(IsWindow(viewer), "refresh keeps log viewer open"); SendDlgItemMessageW(viewer, 601, CB_SETCURSEL, 1, 0); SendMessageW(viewer, WM_COMMAND, MAKEWPARAM(601, CBN_SELCHANGE), (LPARAM)GetDlgItem(viewer, 601)); require(IsWindow(viewer) && GetWindowTextLengthW(GetDlgItem(viewer, 611)) > 0, "previous log or explicit empty state is displayed"); if (argc > 2 && std::string(argv[2]) == "--clear-test") { require(std::filesystem::path(ks::executable()).parent_path().filename() == L"log-clear-test", "clear test runs in isolated executable directory"); const auto logRoot = ks::dataDirectory(); require(logRoot == std::filesystem::path(ks::executable()).parent_path() / L"data", "clear test uses isolated portable data"); std::ofstream(logRoot / L"kswitcher.log") << "current-marker\n"; std::ofstream(logRoot / L"kswitcher.log.1") << "previous-marker\n"; ks::diagnosticEvent(L"buffered-before-clear"); SendMessageW(viewer, WM_COMMAND, MAKEWPARAM(606, BN_CLICKED), (LPARAM)GetDlgItem(viewer, 606)); require(!std::filesystem::exists(logRoot / L"kswitcher.log.1") && ks::readDiagnosticLog(false).find(L"current-marker") != std::wstring::npos, "Clear log removes selected previous log only"); SendDlgItemMessageW(viewer, 601, CB_SETCURSEL, 0, 0); SendMessageW(viewer, WM_COMMAND, MAKEWPARAM(606, BN_CLICKED), (LPARAM)GetDlgItem(viewer, 606)); require(!std::filesystem::exists(logRoot / L"kswitcher.log") && IsWindow(viewer), "Clear log clears current file without closing viewer"); SendMessageW(viewer, WM_COMMAND, MAKEWPARAM(606, BN_CLICKED), (LPARAM)GetDlgItem(viewer, 606)); ks::diagnosticEvent(L"new-problem", true); auto newLog = ks::readDiagnosticLog(false); require(newLog.find(L"new-problem") != std::wstring::npos && newLog.find(L"buffered-before-clear") == std::wstring::npos, "logging resumes after clear without stale context"); ks::DiagnosticJson operation{{"attempt", "1"}, {"process", "alpha.exe"}, {"command", "Convert / cycle"}, {"source", "English"}, {"target", "Ukrainian"}, {"outcome", "Paste verified"}, {"problem", false}, {"original", "ghbdsn"}, {"expected", ks::utf8(L"привіт")}, {"actual", ks::utf8(L"привіт")}}; ks::clearDiagnosticLog(false); const auto successfulOperation = operation; for (int attempt = 0; attempt < 1000; ++attempt) ks::diagnosticOperation(operation.dump()); require(!std::filesystem::exists(logRoot / L"kswitcher.log"), "one thousand successful operations create no log file"); operation["attempt"] = "2"; operation["process"] = "beta.exe"; operation["outcome"] = "Original text remained"; operation["problem"] = true; operation["actual"] = operation["original"]; ks::diagnosticOperation(operation.dump()); auto problems = ks::parseOperationLog(ks::readDiagnosticLog(false)); require(problems.size() == 1 && problems[0].process == L"beta.exe" && problems[0].details.find(L"Completed operation") != std::wstring::npos && problems[0].details.find(L"Paste verified") != std::wstring::npos, "failure writes one row with bounded preceding successful context"); // Pre-existing successful records remain viewable and filterable. ks::log(L"Operation " + ks::wide(successfulOperation.dump())); SendMessageW(viewer, WM_COMMAND, MAKEWPARAM(603, BN_CLICKED), 0); require(ListView_GetItemCount(rows) == 2, "one visible row per recorded operation"); CheckDlgButton(viewer, 610, BST_CHECKED); SendMessageW(viewer, WM_COMMAND, MAKEWPARAM(610, BN_CLICKED), 0); require(ListView_GetItemCount(rows) == 1, "only problems hides verified operations"); SendDlgItemMessageW(viewer, 607, CB_SETCURSEL, 1, 0); SendMessageW(viewer, WM_COMMAND, MAKEWPARAM(607, CBN_SELCHANGE), 0); require(ListView_GetItemCount(rows) == 0, "program filter combines with problems filter"); SendMessageW(viewer, WM_COMMAND, MAKEWPARAM(612, BN_CLICKED), 0); require(ListView_GetItemCount(rows) == 2, "reset restores all operation rows"); NMLISTVIEW sort{}; sort.hdr = {rows, 602, LVN_COLUMNCLICK}; sort.iSubItem = 1; SendMessageW(viewer, WM_NOTIFY, 602, (LPARAM)&sort); wchar_t firstProgram[64]{}; ListView_GetItemText(rows, 0, 1, firstProgram, 64); require(std::wstring(firstProgram) == L"alpha.exe", "clicking program column sorts rows"); require(GetWindowTextLengthW(GetDlgItem(viewer, 611)) > 0, "selected operation populates detail panel"); } SendDlgItemMessageW(viewer, 601, CB_SETCURSEL, 0, 0); SendMessageW(viewer, WM_COMMAND, MAKEWPARAM(603, BN_CLICKED), 0); auto savedClipboard = ks::readClipboard(viewer); instance.window = viewer; try { ListView_SetItemState(rows, 0, LVIS_SELECTED, LVIS_SELECTED); SendMessageW(viewer, WM_COMMAND, MAKEWPARAM(605, BN_CLICKED), (LPARAM)GetDlgItem(viewer, 605)); require(ks::readClipboard(viewer).text().find(L'\t') != std::wstring::npos, "selected log row copies as tab-separated columns"); } catch (...) { ks::writeClipboard(viewer, savedClipboard); throw; } ks::writeClipboard(viewer, savedClipboard); instance.window = nullptr; SendMessageW(viewer, WM_COMMAND, MAKEWPARAM(IDCANCEL, BN_CLICKED), (LPARAM)GetDlgItem(viewer, IDCANCEL)); require(!IsWindow(viewer) && !instance.logWindow, "Close button closes and releases viewer"); CoUninitialize(); return 0; } for (int i = 0; i < (int)app->layouts.size(); ++i) { auto language = LOWORD((uintptr_t)app->layouts[i].handle); if (language == 0x409) en = i; if (language == 0x419) ru = i; } if (en < 0 || ru < 0) throw std::runtime_error( "Integration test requires installed English US and Russian layouts"); WNDCLASSW cls{}; cls.hInstance = GetModuleHandleW(nullptr); cls.lpfnWndProc = procedure; cls.lpszClassName = L"KSwitcher.TestEditor"; cls.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); RegisterClassW(&cls); host = CreateWindowW(cls.lpszClassName, L"KSwitcher — automated test editor", WS_OVERLAPPEDWINDOW | WS_VISIBLE, 100, 100, 650, 320, nullptr, nullptr, cls.hInstance, nullptr); field = CreateWindowW(L"EDIT", L"", WS_CHILD | WS_VISIBLE | WS_BORDER | ES_MULTILINE | ES_WANTRETURN, 20, 20, 590, 230, host, nullptr, cls.hInstance, nullptr); deadline = GetTickCount64() + 30000; SetWindowSubclass(field, observeEnter, 1, 0); if (argc > 1 && std::string(argv[1]) == "--typing-only") stage = 20; if (argc > 1 && std::string(argv[1]) == "--snippets-only") stage = 30; SetTimer(host, 1, 100, nullptr); app->execute(); } std::filesystem::remove_all(root); } catch (const std::exception &e) { std::cerr << e.what() << '\n'; failed = true; } CoUninitialize(); return failed ? 1 : 0; }