Files
hrynco-kswitcher/tests/integration.cpp
T

679 lines
34 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "app.h"
#include "diagnostics.h"
#include <algorithm>
#include <iostream>
#include <fstream>
#include <commctrl.h>
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;
LRESULT CALLBACK observeEnter(HWND w, UINT message, WPARAM wp, LPARAM lp, UINT_PTR, DWORD_PTR)
{
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<std::pair<std::wstring, std::wstring>> 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<char>(file), std::istreambuf_iterator<char>()};
}
size_t diagnosticStart{};
void require(bool ok, const char *name)
{
if (!ok)
{
wchar_t observed[2048]{};
GetWindowTextW(field, observed, 2048);
std::cerr << "Editor at failure: " << ks::utf8(observed)
<< "; foregroundSame=" << (GetForegroundWindow() == host)
<< "; inputVersion=" << app->inputVersion.load() << '\n';
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)
{
if (typeDuringCorrection && ks::InputRegressionAccess::deferring(*app))
{
typeDuringCorrection = false;
typedDuringCorrection = true;
std::vector<INPUT> inputs;
for (auto [key, up] : std::vector<std::pair<WORD, bool>>{
{'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");
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("\"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);
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");
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);
finish();
break;
case 30:
case 32:
case 34:
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->autoPredictor.configure(L"dictionaries", app->settings.autoLanguages);
app->settings.snippets = {{L";r", stage == 31 ? L"rider .{cursor}" : L"rider .{cursor}{enter}"}};
enterPresses = 0;
ActivateKeyboardLayout(app->layouts[en].handle, 0);
prepare(L"prefix ", false);
{
std::vector<INPUT> inputs;
const std::vector<WORD> keys = stage == 35
? std::vector<WORD>{VK_OEM_1, 'R', 'G', 'H', 'B', 'D', 'T', 'N', VK_SPACE}
: std::vector<WORD>{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(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<INPUT> 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");
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<INPUT> 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;
}