#include "app.h" #include #include #include namespace { ks::App *app{}; HWND host{}, field{}; ks::Clip saved; bool captured = false, failed = false; int stage = 0, ru = -1, en = -1; size_t punctuationCase = 0; 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) 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) { try { if (GetTickCount64() > deadline) throw std::runtime_error("Native integration timed out"); if (app->busy) 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"); app->transform(ks::Case); break; case 4: require(text() == L"HELLO WORLD", "consecutive command reselects verified previous result"); 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("reason=recent-verification-mismatch") != std::string::npos, "failed recent-word verification has explicit outcome"); require(diagnostics.find("copied=\"abcdef\"") != std::string::npos && diagnostics.find("physicalKeys=\"ghbdtn\"") != std::string::npos, "conversion diagnostics contain actual text and physical input for reproduction"); } 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); 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); 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"); finish(); 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) { 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); require(rows && (GetWindowLongPtrW(rows, GWL_STYLE) & LVS_TYPEMASK) == LVS_REPORT, "log viewer uses report ListView"); require(Header_GetItemCount(ListView_GetHeader(rows)) == 4, "log viewer has four columns"); 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) && ListView_GetItemCount(rows) > 0, "refresh populates log rows"); SendDlgItemMessageW(viewer, 601, CB_SETCURSEL, 1, 0); SendMessageW(viewer, WM_COMMAND, MAKEWPARAM(601, CBN_SELCHANGE), (LPARAM)GetDlgItem(viewer, 601)); require(IsWindow(viewer) && ListView_GetItemCount(rows) > 0, "previous log or 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"); } 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; 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; }