Files
hrynco-kswitcher/tests/chat_profiles.cpp
T

378 lines
27 KiB
C++

#include "app.h"
#include "ocr.h"
#include "screenshot.h"
#include "lan_pairing.h"
#include <iostream>
#include <commctrl.h>
namespace
{
HWND findTestWindow(LPCWSTR className, LPCWSTR title)
{
HWND window=nullptr;
while((window=FindWindowExW(nullptr,window,className,title))) {
DWORD process{}; GetWindowThreadProcessId(window,&process);
if(process==GetCurrentProcessId()) return window;
}
return nullptr;
}
int passed{};
void require(bool ok, const char *message)
{
if (!ok) throw std::runtime_error(message);
++passed;
}
void contextChecks()
{
ks::Settings scoped;
scoped.snippets = {{L";sig", L"Global", L"global"},
{L";sig", L"Work", L"teams", {L"MS-Teams.EXE", L"OUTLOOK.exe"}},
{L";sig", L"Personal", L"telegram", {L"telegram"}}};
ks::validateSettings(scoped);
require(ks::snippetAppPriority(scoped.snippets[1], L"ms-teams") > ks::snippetAppPriority(scoped.snippets[0], L"ms-teams"), "app snippet precedes global regardless of list order");
require(ks::snippetAppPriority(scoped.snippets[1], L"Telegram.exe") == 0, "restricted snippet cannot run in another app");
require(ks::snippetAppPriority(scoped.snippets[1], L"Outlook.EXE") == 2, "restricted snippet also runs in its second application");
require(ks::snippetAppPriority(scoped.snippets[0], L"") == 1, "legacy/global snippets remain available");
auto scopedRoundtrip = ks::decodeSettings(ks::encodeSettings(scoped), {});
require(scopedRoundtrip.snippets[1].apps == std::vector<std::wstring>{L"ms-teams", L"outlook"}, "scopes normalize and persist");
require(scopedRoundtrip.snippets[0].apps.empty(), "absent scope remains global");
auto collision = scoped; collision.snippets.push_back({L";SIG", L"Ambiguous", L"other", {L"outlook.exe"}});
bool collisionRejected=false;try { ks::validateSettings(collision); } catch(...) { collisionRejected=true; }
require(collisionRejected, "overlapping app lists with same trigger rejected");
auto emptyScope = scoped; emptyScope.snippets[1].apps={L""};
bool emptyRejected=false;try { ks::validateSettings(emptyScope); } catch(...) { emptyRejected=true; }
require(emptyRejected, "empty explicit restriction cannot become global");
require(ks::telegramWebKey(L"https://web.telegram.org/k/#@Example") == L"telegram-web\n@example", "Telegram Web username normalized");
require(ks::telegramWebKey(L"web.telegram.org/a/#-123456") == L"telegram-web\n-123456", "Telegram Web numeric peer");
for (const auto *url : {L"https://web.telegram.org/k/", L"https://web.telegram.org.evil/k/#@example",
L"http://web.telegram.org/k/#@example", L"https://evil/web.telegram.org/k/#@example",
L"https://web.telegram.org/k/#@example?foo", L"https://web.telegram.org/k/#@",
L"https://web.telegram.org/k/#-", L"https://web.telegram.org/k/#settings"})
require(ks::telegramWebKey(url).empty(), "Reject non-chat or untrusted Telegram Web URL");
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]};
settings.chatCycles[L"telegram-web\n@example"] = {settings.cycle[0], settings.cycle[1]};
std::vector<ks::Layout> 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 included in sync");
auto restored = ks::restoreSyncSettings(ks::sharedSyncSettings(settings), withoutProfiles, installed, false);
require(restored.chatCycles == settings.chatCycles, "sync transfers chat profiles to another PC");
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;
auto saveEnabled=[&] { return IsWindowEnabled(GetDlgItem(parent,IDOK))!=FALSE; };
auto status=[&] { wchar_t text[256]{}; GetDlgItemTextW(parent,261,text,256); return std::wstring(text); };
require(!saveEnabled() && status()==L"No unsaved changes.","new Settings shows clean state with Save disabled");
SendMessageW(parent,WM_COMMAND,IDOK,0);
require(IsWindow(parent),"Save command with no changes does not apply or close Settings");
bool originalLog=IsDlgButtonChecked(parent,208)==BST_CHECKED;
CheckDlgButton(parent,208,originalLog?BST_UNCHECKED:BST_CHECKED);
SendMessageW(parent,WM_COMMAND,MAKEWPARAM(208,BN_CLICKED),0);
require(saveEnabled() && status().find(L"Unsaved changes") == 0,"checkbox edit immediately marks Settings dirty");
CheckDlgButton(parent,208,originalLog?BST_CHECKED:BST_UNCHECKED);
SendMessageW(parent,WM_COMMAND,MAKEWPARAM(208,BN_CLICKED),0);
require(!saveEnabled(),"reverting checkbox disables Save again");
wchar_t originalLimit[32]{}; GetDlgItemTextW(parent,240,originalLimit,32);
SetDlgItemTextW(parent,240,L"invalid");
require(saveEnabled(),"invalid text remains an unsaved edit");
SetDlgItemTextW(parent,240,originalLimit);
require(!saveEnabled(),"restoring valid original text clears dirty state");
SendMessageW(parent,WM_COMMAND,257,0);
HWND ocr=findTestWindow(L"KSwitcher.OcrOptions",nullptr);
require(ocr!=nullptr,"OCR preferences open");
wchar_t languages[128]{}; GetDlgItemTextW(ocr,503,languages,128);
SetDlgItemTextW(ocr,503,L"eng");
SendMessageW(ocr,WM_COMMAND,IDCANCEL,0);
require(!saveEnabled(),"cancelled child changes leave Settings clean");
SendMessageW(parent,WM_COMMAND,257,0);
ocr=findTestWindow(L"KSwitcher.OcrOptions",nullptr);
SetDlgItemTextW(ocr,503,std::wstring(languages)==L"eng"?L"ukr":L"eng");
SendMessageW(ocr,WM_COMMAND,IDOK,0);
require(!IsWindow(ocr) && saveEnabled(),"accepted OCR edits enable parent Save");
SendMessageW(parent,WM_COMMAND,257,0);
ocr=findTestWindow(L"KSwitcher.OcrOptions",nullptr);
SetDlgItemTextW(ocr,503,languages);
SendMessageW(ocr,WM_COMMAND,IDOK,0);
require(!saveEnabled(),"reverting OCR draft in child clears parent dirty state");
SendMessageW(parent,WM_COMMAND,255,0);
HWND automatic=findTestWindow(L"KSwitcher.AutoOptions",nullptr);
require(automatic!=nullptr,"automatic correction preferences open");
bool replace=IsDlgButtonChecked(automatic,414)==BST_CHECKED;
CheckDlgButton(automatic,414,replace?BST_UNCHECKED:BST_CHECKED);
SendMessageW(automatic,WM_COMMAND,IDOK,0);
require(!IsWindow(automatic) && saveEnabled(),"accepted auto-correction edits enable Save");
SendMessageW(parent,WM_COMMAND,255,0);
automatic=findTestWindow(L"KSwitcher.AutoOptions",nullptr);
CheckDlgButton(automatic,414,replace?BST_CHECKED:BST_UNCHECKED);
SendMessageW(automatic,WM_COMMAND,IDOK,0);
require(!saveEnabled(),"reverting auto-correction draft disables Save");
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)&notification);
std::vector<RECT> 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)&notification);
require(!saveEnabled(),"changing tabs leaves Settings clean");
SendMessageW(parent, WM_COMMAND, 259, 0);
HWND dialog = findTestWindow(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");
require(!saveEnabled(),"cancelled chat edits do not enable Save");
SendMessageW(parent, WM_COMMAND, 259, 0);
dialog = findTestWindow(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");
require(saveEnabled() == (app.layouts.size()>1),"accepted chat edits refresh Save immediately");
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;
require(!saveEnabled(),"reopened saved Settings is clean");
SendMessageW(parent, WM_COMMAND, 259, 0);
dialog = findTestWindow(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);
require(saveEnabled() && status().find(L"Unsaved changes")==0,"removing child profile immediately marks parent dirty");
RedrawWindow(parent,nullptr,nullptr,RDW_INVALIDATE|RDW_UPDATENOW|RDW_ALLCHILDREN);
GetClientRect(parent,&rect); dc=GetDC(parent);
clip=ks::screenshotClip(dc,rect.right,rect.bottom,{}); ReleaseDC(parent,dc);
ks::atomicWrite(std::filesystem::path(ks::executable()).parent_path()/L"settings-unsaved.png",
ks::encodePng(ks::decodeImage(clip)));
SendMessageW(parent, WM_COMMAND, IDOK, 0);
require(!app.settings.chatCycles.contains(key), "remove profile restores default");
app.settings.snippets = {{L";scope", L"Example", L"scope-fixture"}};
ks::showSettings(app); parent=app.settingsWindow;
require(!saveEnabled(),"Settings starts clean before separately saved snippet edit");
ks::showSnippets(app);
HWND snippets = app.snippetsWindow;
SendDlgItemMessageW(snippets,301,LB_SETCURSEL,0,0);
SendMessageW(snippets,WM_COMMAND,MAKEWPARAM(301,LBN_SELCHANGE),0);
require(IsDlgButtonChecked(snippets,314)==BST_UNCHECKED, "old snippet shows everywhere");
CheckDlgButton(snippets,314,BST_CHECKED);
SendMessageW(snippets,WM_COMMAND,314,0);
SetDlgItemTextW(snippets,315,L"Notepad.exe");
auto secondApp = SendDlgItemMessageW(snippets,316,CB_ADDSTRING,0,(LPARAM)L"MS-Teams.EXE");
SendDlgItemMessageW(snippets,316,CB_SETCURSEL,secondApp,0);
SendMessageW(snippets,WM_COMMAND,317,0);
SendMessageW(snippets,WM_COMMAND,317,0);
wchar_t appList[128]{};
GetDlgItemTextW(snippets,315,appList,128);
require(std::wstring(appList)==L"Notepad.exe, MS-Teams.EXE", "Add app appends a second application without duplicates");
SendMessageW(snippets,WM_COMMAND,IDOK,0);
require(!app.snippetsWindow && app.settings.snippets[0].apps==std::vector<std::wstring>{L"notepad",L"ms-teams"}, "snippet editor saves selected applications");
require(!saveEnabled() && status()==L"No unsaved changes.","separately saved snippet edit leaves Settings clean");
SendMessageW(parent,WM_COMMAND,IDCANCEL,0);
ks::showSnippets(app);snippets=app.snippetsWindow;
SendDlgItemMessageW(snippets,301,LB_SETCURSEL,0,0);
SendMessageW(snippets,WM_COMMAND,MAKEWPARAM(301,LBN_SELCHANGE),0);
require(IsDlgButtonChecked(snippets,314)==BST_CHECKED, "scope restored when reopening editor");
RedrawWindow(snippets,nullptr,nullptr,RDW_INVALIDATE|RDW_UPDATENOW|RDW_ALLCHILDREN);
RECT snippetRect{};GetClientRect(snippets,&snippetRect);
HDC snippetDc=GetDC(snippets);
auto snippetImage=ks::screenshotClip(snippetDc,snippetRect.right,snippetRect.bottom,{});
ReleaseDC(snippets,snippetDc);
ks::atomicWrite(std::filesystem::path(ks::executable()).parent_path()/L"snippet-scopes.png",ks::encodePng(ks::decodeImage(snippetImage)));
SetDlgItemTextW(snippets,315,L"chrome.exe");
SendMessageW(snippets,WM_COMMAND,IDCANCEL,0);
require(app.settings.snippets[0].apps==std::vector<std::wstring>{L"notepad",L"ms-teams"}, "cancel preserves existing scope");
ks::showSettings(app);parent=app.settingsWindow;
SendMessageW(parent,WM_COMMAND,262,0);
HWND lan=findTestWindow(L"KSwitcher.LAN",nullptr);
require(lan && !IsWindowEnabled(parent),"LAN pairing opens a native child window");
require(!IsWindowEnabled(GetDlgItem(lan,705)),"pairing disabled without discovered peer");
require(!IsWindowEnabled(GetDlgItem(lan,706)),"no cancellation without pending pairing");
require(!IsWindowEnabled(GetDlgItem(lan,707)),"cannot forget nonexistent pair");
require(!IsWindowEnabled(GetDlgItem(lan,715)),"input sharing requires a paired computer");
require(!IsWindowEnabled(GetDlgItem(lan,716)),"stop input disabled when sharing is off");
require(GetDlgItem(lan,721)!=nullptr,"monitor arrangement grid is present");
require(!(GetWindowLongPtrW(lan,GWL_STYLE)&(WS_THICKFRAME|WS_MAXIMIZEBOX)),"LAN window has a fixed size");
require(GetDlgItem(lan,711)==nullptr,"obsolete side radios are removed");
require(SendDlgItemMessageW(lan,702,LB_GETCOUNT,0,0)==0,"disabled discovery lists no generic network devices");
require(!std::filesystem::exists(root/L"lan-pairing.bin"),"opening LAN settings does not enable or save discovery");
RedrawWindow(lan,nullptr,nullptr,RDW_INVALIDATE|RDW_UPDATENOW|RDW_ALLCHILDREN);
RECT lanRect{};GetClientRect(lan,&lanRect);HDC lanDc=GetDC(lan);
auto lanImage=ks::screenshotClip(lanDc,lanRect.right,lanRect.bottom,{});ReleaseDC(lan,lanDc);
ks::atomicWrite(std::filesystem::path(ks::executable()).parent_path()/L"lan-pairing.png",ks::encodePng(ks::decodeImage(lanImage)));
SendMessageW(lan,WM_COMMAND,IDCANCEL,0);
require(IsWindowEnabled(parent)&&!saveEnabled(),"closing discovery restores Settings without false unsaved state");
SendMessageW(parent,WM_COMMAND,IDCANCEL,0);
}
{
ks::lan::Json fixture={{"version",1},{"id",std::string(32,'a')},{"enabled",false},{"inputPeer",std::string(32,'b')},{"inputEdge",1},
{"trusted",{{std::string(32,'b'),{{"name","Test paired KSwitcher"},{"key",std::string(64,'f')}}}}}};
auto text=fixture.dump();ks::atomicWrite(root/L"lan-pairing.bin",ks::protect(ks::Bytes(text.begin(),text.end())));
ks::App pairedApp(GetModuleHandleW(nullptr),true,root);ks::showSettings(pairedApp);
SendMessageW(pairedApp.settingsWindow,WM_COMMAND,262,0);
HWND lan=findTestWindow(L"KSwitcher.LAN",nullptr);
require(SendDlgItemMessageW(lan,702,LB_GETCURSEL,0,0)==0,"single saved pair selected automatically when opening LAN settings");
wchar_t pairState[512]{};SendDlgItemMessageW(lan,702,LB_GETTEXT,0,(LPARAM)pairState);
require(std::wstring(pairState).find(L"Pair saved — input disabled here")!=std::wstring::npos,"saved pairing does not imply an active input connection");
require(!IsWindowEnabled(GetDlgItem(lan,705))&&!IsWindowEnabled(GetDlgItem(lan,703))&&!IsWindowEnabled(GetDlgItem(lan,704)),"saved pair disables pairing and code controls");
SendDlgItemMessageW(lan,702,LB_SETCURSEL,-1,0);
SendMessageW(lan,WM_TIMER,1,0);
require(SendDlgItemMessageW(lan,702,LB_GETCURSEL,0,0)==0,"refresh restores single paired selection without a peer list change");
auto grid=GetDlgItem(lan,721);
SendMessageW(grid,WM_KEYDOWN,VK_UP,0);SendMessageW(grid,WM_KEYDOWN,VK_LEFT,0);
auto saved=ks::lan::Json::parse(ks::unprotect(ks::readFile(root/L"lan-pairing.bin",256*1024)));
require(saved["positions"][std::string(32,'b')]==ks::lan::Json::array({0,-1}),"keyboard moves monitor above this PC and saves immediately");
SendMessageW(lan,WM_TIMER,1,0);
require(saved["inputEdge"]==2,"monitor arrangement updates the existing input direction");
require(!IsWindowEnabled(GetDlgItem(lan,715)),"input cannot start while discovery is disabled");
// Let the native radio-button theme animation finish before visual capture.
auto repaintUntil=GetTickCount64()+200;
while(GetTickCount64()<repaintUntil)
{
MSG message{};
while(PeekMessageW(&message,nullptr,0,0,PM_REMOVE)){TranslateMessage(&message);DispatchMessageW(&message);}
Sleep(10);
}
RedrawWindow(lan,nullptr,nullptr,RDW_INVALIDATE|RDW_UPDATENOW|RDW_ALLCHILDREN);
RECT r{};GetClientRect(lan,&r);HDC dc=GetDC(lan);auto screenshot=ks::screenshotClip(dc,r.right,r.bottom,{});ReleaseDC(lan,dc);
ks::atomicWrite(std::filesystem::path(ks::executable()).parent_path()/L"lan-screen-layout.png",ks::encodePng(ks::decodeImage(screenshot)));
SendMessageW(lan,WM_COMMAND,IDCANCEL,0);
SendMessageW(pairedApp.settingsWindow,WM_COMMAND,IDCANCEL,0);
}
{
using J=ks::lan::Json;const std::string b(32,'b'),c(32,'c');
J fixture={{"version",1},{"id",std::string(32,'a')},{"enabled",false},{"inputPeer",b},{"inputEdge",0},
{"trusted",{{b,{{"name","Left desktop"},{"key",std::string(64,'f')}}},{c,{{"name","Right laptop"},{"key",std::string(64,'e')}}}}},
{"positions",{{b,J::array({-1,0})},{c,J::array({1,0})}}}};
auto text=fixture.dump();ks::atomicWrite(root/L"lan-pairing.bin",ks::protect(ks::Bytes(text.begin(),text.end())));
ks::App layoutApp(GetModuleHandleW(nullptr),true,root);ks::showSettings(layoutApp);
SendMessageW(layoutApp.settingsWindow,WM_COMMAND,262,0);auto lan=findTestWindow(L"KSwitcher.LAN",nullptr);auto grid=GetDlgItem(lan,721);
RECT r{};GetClientRect(grid,&r);
auto point=[&](int column,int row){return MAKELPARAM((2*column+1)*r.right/10,(2*row+1)*r.bottom/6);};
SendMessageW(grid,WM_LBUTTONDOWN,MK_LBUTTON,point(1,1));SendMessageW(grid,WM_MOUSEMOVE,MK_LBUTTON,point(1,0));SendMessageW(grid,WM_LBUTTONUP,0,point(1,0));
auto saved=J::parse(ks::unprotect(ks::readFile(root/L"lan-pairing.bin",256*1024)));
require(saved["positions"][b]==J::array({-1,-1})&&saved["positions"][c]==J::array({1,0}),"drag moves one monitor without moving another paired PC");
RedrawWindow(lan,nullptr,nullptr,RDW_INVALIDATE|RDW_UPDATENOW|RDW_ALLCHILDREN);
GetClientRect(lan,&r);auto dc=GetDC(lan);auto screenshot=ks::screenshotClip(dc,r.right,r.bottom,{});ReleaseDC(lan,dc);
ks::atomicWrite(std::filesystem::path(ks::executable()).parent_path()/L"lan-three-computers.png",ks::encodePng(ks::decodeImage(screenshot)));
SendMessageW(lan,WM_COMMAND,IDCANCEL,0);SendMessageW(layoutApp.settingsWindow,WM_COMMAND,IDCANCEL,0);
}
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; }
}