#include "app.h" #include "ocr.h" #include "screenshot.h" #include #include namespace { int passed{}; void require(bool ok, const char *message) { if (!ok) throw std::runtime_error(message); ++passed; } void contextChecks() { const auto key = ks::chatKey(L"Telegram.exe", L"\u200eТестовий чат \u2013 (12)"); require(key == L"telegram\nТестовий чат", "Telegram unread suffix"); require(ks::chatKey(L"Telegram", L"Тестовий чат") == key, "stable unread profile"); require(ks::chatKey(L"Telegram", L"Team (2026)") == L"telegram\nTeam (2026)", "preserve parentheses"); require(ks::chatKey(L"Telegram", L"Telegram").empty(), "no profile on home"); require(ks::chatKey(L"Viber", L"Rakuten Viber").empty(), "Viber generic window title is not a chat"); require(ks::chatKey(L"ms-teams", L"Chat | Alice | Microsoft Teams") == L"ms-teams\nAlice", "Teams chat"); require(ks::chatKey(L"ms-teams", L"Calendar | Alice | Microsoft Teams").empty(), "Teams calendar excluded"); require(ks::chatKey(L"ms-teams", L"Chat | Microsoft Teams").empty(), "Teams chat home excluded"); require(ks::chatKey(L"other", L"Chat | Alice | Microsoft Teams").empty(), "app identity required"); ks::Settings settings; settings.cycle = {L"win:04090409", L"win:04220422", L"win:04190419"}; settings.chatCycles[key] = {settings.cycle[1], settings.cycle[0]}; settings.chatCycles[L"viber\nТестовий контакт"] = {settings.cycle[1], settings.cycle[0]}; std::vector installed(3); for (size_t i = 0; i < installed.size(); ++i) { installed[i].id = settings.cycle[i]; installed[i].handle = (HKL)(uintptr_t)(i + 1); } auto subset = settings.chatCycles[key]; require(ks::nextChatLayout(subset, installed, installed[1].handle, false) == installed[0].handle, "cycle within subset"); require(ks::nextChatLayout(subset, installed, installed[0].handle, false) == installed[1].handle, "wrap skips excluded language"); require(ks::nextChatLayout(subset, installed, installed[2].handle, false) == installed[1].handle, "outside subset enters first language"); require(ks::nextChatLayout(settings.cycle, installed, installed[0].handle, true) == installed[2].handle, "reverse wraps"); require(ks::nextChatLayout({settings.cycle[1]}, installed, installed[0].handle, false) == installed[1].handle, "single language selects allowed layout"); require(!ks::nextChatLayout({L"win:04070407"}, installed, installed[0].handle, false), "missing layout is not activated"); require(ks::nextConversionLayout(L"", installed, {1}, 0) == 1, "conversion uses single allowed language"); require(ks::chatCycle(settings, key, installed) == settings.chatCycles[key], "profile order"); require(ks::chatCycle(settings, L"telegram\nOther", installed) == settings.cycle, "unknown fallback"); require(ks::decodeSettings(ks::encodeSettings(settings), installed).chatCycles == settings.chatCycles, "Unicode roundtrip"); require(ks::decodeSettings({}, installed).chatCycles.empty(), "old defaults without profiles"); auto withoutProfiles = settings; withoutProfiles.chatCycles.clear(); require(ks::sharedSyncSettings(settings) == ks::sharedSyncSettings(withoutProfiles), "profiles excluded from sync"); auto restored = ks::restoreSyncSettings(ks::sharedSyncSettings(settings), settings, installed, false); require(restored.chatCycles == settings.chatCycles, "sync preserves local profiles"); settings.chatCycles[key] = {settings.cycle[1]}; require(ks::chatCycle(settings, key, installed).size() == 1, "one-language profile"); settings.chatCycles[key] = {L"win:04070407"}; require(ks::chatCycle(settings, key, installed) == settings.cycle, "missing layouts fallback"); settings.chatCycles[key] = {}; bool rejected = false; try { ks::encodeSettings(settings); } catch (...) { rejected = true; } require(rejected, "empty profile rejected"); settings.chatCycles[key] = {settings.cycle[0], settings.cycle[0]}; rejected = false; try { ks::encodeSettings(settings); } catch (...) { rejected = true; } require(rejected, "duplicate profile layout rejected"); ks::ChatSwitchHook hook; require(hook.update(VK_SPACE, true, MOD_WIN, false) == 0, "unprofiled Win+Space passes"); require(hook.update(VK_SPACE, true, MOD_WIN, true) == 1, "Win+Space forward"); require(hook.update(VK_SPACE, true, MOD_WIN, true) == -1, "consume repeat"); require(hook.update(VK_SPACE, false, 0, false) == -1, "release after profile change"); require(hook.update(VK_SPACE, true, MOD_WIN | MOD_SHIFT, true) == 2, "reverse cycle"); hook.update(VK_SPACE, false, 0, true); require(hook.update(VK_LSHIFT, true, MOD_ALT, true) == 1, "Alt then Shift"); hook.update(VK_LSHIFT, false, 0, true); require(hook.update(VK_LMENU, true, MOD_SHIFT, true) == 1, "Shift then Alt"); hook.update(VK_LMENU, false, 0, true); require(hook.update(VK_LSHIFT, true, MOD_ALT | MOD_CONTROL, true) == 0, "do not consume Ctrl+Alt+Shift"); require(hook.update('A', true, MOD_ALT, true) == 0, "ordinary keys pass"); } } int main() { CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); auto root = std::filesystem::temp_directory_path() / (L"KSwitcher-chat-tests-" + std::to_wstring(GetCurrentProcessId())); try { contextChecks(); { ks::App app(GetModuleHandleW(nullptr), true, root); require(!app.layouts.empty(), "installed layouts available"); app.settings.toggle = app.settings.casing = app.settings.translit = app.settings.undo = L""; app.settings.clipboard = app.settings.screenshot = L""; app.settings.direct.assign(app.layouts.size(), L""); app.settings.history = app.settings.autoCorrect = false; const std::wstring key = L"telegram\n! Fixture chat"; app.settings.chatCycles[key] = {app.layouts[0].id}; ks::showSettings(app); HWND parent = app.settingsWindow; HWND tabs = GetDlgItem(parent, 260); require(tabs && TabCtrl_GetItemCount(tabs) == 5, "five settings categories"); for (int page = 0; page < 5; ++page) { TabCtrl_SetCurSel(tabs, page); NMHDR notification{tabs, 260, TCN_SELCHANGE}; SendMessageW(parent, WM_NOTIFY, 260, (LPARAM)¬ification); std::vector boxes; for (HWND child = GetWindow(parent, GW_CHILD); child; child = GetWindow(child, GW_HWNDNEXT)) { if (!(GetWindowLongPtrW(child, GWL_STYLE) & WS_VISIBLE)) continue; RECT rect{}; GetWindowRect(child, &rect); for (const auto &other : boxes) { RECT overlap{}; require(!IntersectRect(&overlap, &rect, &other), "visible settings controls never overlap"); } boxes.push_back(rect); } RedrawWindow(parent, nullptr, nullptr, RDW_INVALIDATE | RDW_UPDATENOW | RDW_ALLCHILDREN); RECT rect{}; GetClientRect(parent, &rect); HDC dc = GetDC(parent); auto clip = ks::screenshotClip(dc, rect.right, rect.bottom, {}); ReleaseDC(parent, dc); ks::atomicWrite(std::filesystem::path(ks::executable()).parent_path() / (L"settings-tab-" + std::to_wstring(page) + L".png"), ks::encodePng(ks::decodeImage(clip))); } TabCtrl_SetCurSel(tabs, 1); NMHDR notification{tabs, 260, TCN_SELCHANGE}; SendMessageW(parent, WM_NOTIFY, 260, (LPARAM)¬ification); SendMessageW(parent, WM_COMMAND, 259, 0); HWND dialog = FindWindowW(L"KSwitcher.ChatLanguages", nullptr); require(dialog && !IsWindowEnabled(parent), "chat dialog opens with disabled parent"); int index = (int)SendDlgItemMessageW(dialog, 710, LB_FINDSTRINGEXACT, -1, (LPARAM)L"Telegram: ! Fixture chat"); require(index >= 0, "saved chat listed"); SendDlgItemMessageW(dialog, 710, LB_SETCURSEL, index, 0); SendMessageW(dialog, WM_COMMAND, MAKEWPARAM(710, LBN_SELCHANGE), 0); require(SendDlgItemMessageW(dialog, 711, LB_GETSELCOUNT, 0, 0) == 1, "saved single language selected"); RedrawWindow(dialog, nullptr, nullptr, RDW_INVALIDATE | RDW_UPDATENOW | RDW_ALLCHILDREN); RECT rect{}; GetClientRect(dialog, &rect); HDC dc = GetDC(dialog); auto clip = ks::screenshotClip(dc, rect.right, rect.bottom, {}); ReleaseDC(dialog, dc); ks::atomicWrite(std::filesystem::path(ks::executable()).parent_path() / L"chat-languages.png", ks::encodePng(ks::decodeImage(clip))); SendDlgItemMessageW(dialog, 711, LB_SETSEL, TRUE, -1); SendMessageW(dialog, WM_COMMAND, IDCANCEL, 0); require(!IsWindow(dialog) && IsWindowEnabled(parent), "cancel closes dialog"); SendMessageW(parent, WM_COMMAND, 259, 0); dialog = FindWindowW(L"KSwitcher.ChatLanguages", nullptr); index = (int)SendDlgItemMessageW(dialog, 710, LB_FINDSTRINGEXACT, -1, (LPARAM)L"Telegram: ! Fixture chat"); SendDlgItemMessageW(dialog, 710, LB_SETCURSEL, index, 0); SendMessageW(dialog, WM_COMMAND, MAKEWPARAM(710, LBN_SELCHANGE), 0); require(SendDlgItemMessageW(dialog, 711, LB_GETSELCOUNT, 0, 0) == 1, "cancel discards language edits"); SendDlgItemMessageW(dialog, 711, LB_SETSEL, TRUE, -1); SendMessageW(dialog, WM_COMMAND, IDOK, 0); require(!IsWindow(dialog) && IsWindowEnabled(parent), "profile applies to settings draft"); SendMessageW(parent, WM_COMMAND, IDOK, 0); require(!app.settingsWindow && app.settings.chatCycles.at(key).size() == app.layouts.size(), "settings saves selected languages"); require(ks::loadSettings(root / L"settings.json", app.layouts).chatCycles == app.settings.chatCycles, "profile saved to disk"); ks::showSettings(app); parent = app.settingsWindow; SendMessageW(parent, WM_COMMAND, 259, 0); dialog = FindWindowW(L"KSwitcher.ChatLanguages", nullptr); index = (int)SendDlgItemMessageW(dialog, 710, LB_FINDSTRINGEXACT, -1, (LPARAM)L"Telegram: ! Fixture chat"); SendDlgItemMessageW(dialog, 710, LB_SETCURSEL, index, 0); SendMessageW(dialog, WM_COMMAND, MAKEWPARAM(710, LBN_SELCHANGE), 0); SendMessageW(dialog, WM_COMMAND, 713, 0); SendMessageW(parent, WM_COMMAND, IDOK, 0); require(!app.settings.chatCycles.contains(key), "remove profile restores default"); } std::filesystem::remove_all(root); std::cout << "Passed " << passed << " chat profile checks\n"; CoUninitialize(); return 0; } catch (const std::exception &e) { std::cerr << "FAIL: " << e.what() << '\n'; CoUninitialize(); return 1; } }