// SPDX-License-Identifier: LGPL-2.1-or-later #include #include #include #ifdef WINE_ACCESS_WINFORMS_FIXTURE static const WCHAR *editClass = L"WindowsForms10.EDIT.app.0.fixture_r7_ad1"; static const WCHAR *staticClass = L"WindowsForms10.STATIC.app.0.fixture_r7_ad1"; static void register_control(HINSTANCE instance, const WCHAR *nativeName, const WCHAR *managedName) { WNDCLASSEXW cls = {0}; cls.cbSize = sizeof(cls); if (!GetClassInfoExW(NULL, nativeName, &cls)) ExitProcess(3); cls.hInstance = instance; cls.lpszClassName = managedName; if (!RegisterClassExW(&cls)) ExitProcess(4); } #else static const WCHAR *editClass = L"Edit"; #endif static HWND editor, password, history; static int step; static int history_position; static DWORD WINAPI update_history_from_worker(void *window) { SendMessageW((HWND)window, WM_SETTEXT, 0, (LPARAM)L"unrelated worker update"); return 0; } static void unrelated_history_update(void) { HANDLE thread = CreateThread(NULL, 0, update_history_from_worker, history, 0, NULL); if (!thread) ExitProcess(2); // Dispatch sent messages without removing a queued message, as GetMessage // does while waiting for input. Do not rely on worker/timer scheduling. for (;;) { DWORD result = MsgWaitForMultipleObjects(1, &thread, FALSE, 2000, QS_SENDMESSAGE); if (result == WAIT_OBJECT_0) break; if (result != WAIT_OBJECT_0 + 1) ExitProcess(2); MSG message; PeekMessageW(&message, NULL, 0, 0, PM_NOREMOVE); } CloseHandle(thread); WCHAR text[64]; GetWindowTextW(history, text, 64); if (lstrcmpW(text, L"unrelated worker update")) ExitProcess(2); } // Model MUSHclient's PreTranslateMessage: consume queued arrows before the // Edit procedure, then use SetWindowText and SetSel to recall a command. static BOOL history_input(const MSG *message) { if (message->hwnd != history || message->message != WM_KEYDOWN || (message->wParam != VK_UP && message->wParam != VK_DOWN)) return FALSE; if (message->wParam == VK_UP && history_position < 2) ++history_position; else if (message->wParam == VK_DOWN && history_position > 0) --history_position; else { if (step == 49) unrelated_history_update(); return TRUE; } const WCHAR *commands[] = {L"", L"history north", L"history south"}; SetWindowTextW(history, commands[history_position]); int length = GetWindowTextLengthW(history); SendMessageW(history, EM_SETSEL, length, length); return TRUE; } static void key(UINT key, BOOL control, BOOL shift) { BYTE saved[256], state[256]; GetKeyboardState(saved); CopyMemory(state, saved, sizeof(state)); state[VK_CONTROL] = control ? 0x80 : 0; state[VK_SHIFT] = shift ? 0x80 : 0; SetKeyboardState(state); SendMessageW(editor, WM_KEYDOWN, key, 1); SetKeyboardState(saved); } static LRESULT CALLBACK window_proc(HWND window, UINT message, WPARAM wp, LPARAM lp) { if (message == WM_TIMER) { if (step == 0) SetTimer(window, 1, 350, NULL); switch (++step) { case 1: key(VK_RIGHT, FALSE, FALSE); break; case 2: key(VK_RIGHT, TRUE, FALSE); break; case 3: key(VK_DOWN, FALSE, FALSE); break; case 4: key(VK_HOME, FALSE, FALSE); break; case 5: key(VK_RIGHT, FALSE, TRUE); break; case 6: key(VK_RIGHT, FALSE, TRUE); break; case 7: key(VK_LEFT, FALSE, TRUE); break; case 8: key(VK_LEFT, FALSE, FALSE); break; case 9: key(VK_DELETE, FALSE, FALSE); break; case 10: key(VK_RIGHT, FALSE, FALSE); SendMessageW(editor, WM_CHAR, VK_BACK, 1); break; case 11: SendMessageW(editor, EM_SETSEL, 0, 5); break; case 12: SendMessageW(editor, WM_CLEAR, 0, 0); break; case 13: SetFocus(password); break; case 14: SendMessageW(password, WM_KEYDOWN, VK_LEFT, 1); break; case 15: SendMessageW(password, WM_CHAR, VK_BACK, 1); break; case 16: SetFocus(editor); SetWindowTextW(editor, L"\U0001f600 cafe"); SendMessageW(editor, EM_SETSEL, 2, 2); break; case 17: key(VK_HOME, FALSE, FALSE); break; case 18: SendMessageW(editor, EM_SETREADONLY, TRUE, 0); SetFocus(password); SetFocus(editor); break; case 19: key(VK_DELETE, FALSE, FALSE); break; case 20: if (GetWindowTextLengthW(editor) != 7) ExitProcess(2); SendMessageW(editor, EM_SETREADONLY, FALSE, 0); SetWindowTextW(editor, L"hello\r\nmenu return line"); SendMessageW(editor, EM_SETSEL, 0, 0); break; case 21: key(VK_RIGHT, FALSE, FALSE); key(VK_RIGHT, FALSE, FALSE); key(VK_RIGHT, FALSE, FALSE); break; case 22: SendMessageW(editor, EM_SETSEL, 7, 7); SendMessageW(window, WM_MENUSELECT, MAKEWPARAM(1, 0), (LPARAM)GetMenu(window)); break; case 23: SendMessageW(window, WM_EXITMENULOOP, 0, 0); break; case 24: // Replacement with shorter text is not a Delete/Backspace command. SendMessageW(editor, EM_SETSEL, 0, 5); SendMessageW(editor, EM_REPLACESEL, TRUE, (LPARAM)L"hi"); break; case 25: SetWindowTextW(editor, L"first\r\nsecond"); SendMessageW(editor, EM_SETSEL, 7, 7); SendMessageW(editor, WM_CHAR, VK_BACK, 1); break; case 26: SetWindowTextW(editor, L"first\r\nsecond"); SendMessageW(editor, EM_SETSEL, 5, 5); key(VK_DELETE, FALSE, FALSE); break; case 27: // A surrogate at the 1023-unit line boundary must not discard the line. { WCHAR line[1027]; for (int i = 0; i < 1022; ++i) line[i] = L'x'; line[1022] = 0xd83d; line[1023] = 0xde00; line[1024] = L'y'; line[1025] = 0; SetWindowTextW(editor, line); SendMessageW(editor, EM_SETSEL, 0, 0); SetFocus(password); SetFocus(editor); } break; case 28: SetWindowTextW(editor, L"undo restored"); SendMessageW(editor, EM_SETSEL, 0, 4); SendMessageW(editor, EM_REPLACESEL, TRUE, (LPARAM)L"x"); SendMessageW(editor, EM_UNDO, 0, 0); SendMessageW(editor, EM_SETSEL, 1, 1); break; case 29: key(VK_RIGHT, FALSE, FALSE); // d, not the space in the old snapshot break; case 30: key(VK_END, TRUE, FALSE); // whole restored line break; case 31: SetWindowTextW(editor, L"readonly first\r\nreadonly second"); SendMessageW(editor, EM_SETSEL, 0, 0); SendMessageW(editor, EM_SETREADONLY, TRUE, 0); SetFocus(password); SetFocus(editor); break; case 32: key(VK_DOWN, FALSE, FALSE); break; case 33: key(VK_UP, FALSE, FALSE); break; case 34: key(VK_DELETE, FALSE, FALSE); if (GetWindowTextLengthW(editor) != 31) ExitProcess(2); break; case 35: SendMessageW(editor, WM_CHAR, L'!', 1); if (GetWindowTextLengthW(editor) != 31) ExitProcess(2); break; case 36: SendMessageW(editor, EM_SETREADONLY, FALSE, 0); SetWindowTextW(editor, L""); SendMessageW(editor, WM_CHAR, L'z', 1); SendMessageW(editor, WM_CHAR, L'z', 1); SendMessageW(editor, WM_CHAR, L' ', 1); SendMessageW(editor, WM_CHAR, 0x00e9, 1); SendMessageW(editor, WM_CHAR, 0xd83d, 1); SendMessageW(editor, WM_CHAR, 0xde00, 1); SendMessageW(editor, WM_CHAR, VK_RETURN, 1); SendMessageW(editor, WM_CHAR, VK_TAB, 1); { WCHAR text[32]; GetWindowTextW(editor, text, 32); if (lstrcmpW(text, L"zz \u00e9\U0001f600\r\n\t")) ExitProcess(2); } break; case 37: // Replacing selected text is still typing, even when length shrinks. SendMessageW(editor, EM_SETSEL, 0, 4); SendMessageW(editor, WM_CHAR, L'q', 1); break; case 38: SetFocus(password); SendMessageW(password, WM_CHAR, L'@', 1); // Background changes must not produce echo for the focused password. SendMessageW(editor, WM_CHAR, L'~', 1); break; case 39: SetFocus(editor); { DWORD length = 1024 * 1024 + 1; WCHAR *large = calloc(length + 1, sizeof(WCHAR)); if (!large) ExitProcess(2); for (DWORD i = 0; i < length; ++i) large[i] = L'x'; SendMessageW(editor, EM_SETLIMITTEXT, length + 3, 0); SetWindowTextW(editor, large); free(large); SendMessageW(editor, EM_SETSEL, length, length); SendMessageW(editor, WM_CHAR, L'L', 1); SendMessageW(editor, WM_CHAR, 0xd83d, 1); SendMessageW(editor, WM_CHAR, 0xde00, 1); if (GetWindowTextLengthW(editor) != (int)length + 3) { fprintf(stderr, "large typing: length=%d expected=%lu limit=%ld\n", GetWindowTextLengthW(editor), (unsigned long)length + 3, (long)SendMessageW(editor, EM_GETLIMITTEXT, 0, 0)); ExitProcess(2); } } break; case 40: SetWindowTextW(editor, L"full"); SendMessageW(editor, EM_SETSEL, 4, 4); SendMessageW(editor, EM_SETLIMITTEXT, 4, 0); SendMessageW(editor, WM_CHAR, L'%', 1); if (GetWindowTextLengthW(editor) != 4) ExitProcess(2); break; case 41: // Keep this field away from the managed-label regression's static labels. history = CreateWindowW(editClass, L"", WS_CHILD | WS_VISIBLE | ES_AUTOHSCROLL, 100, 150, 300, 25, window, NULL, GetModuleHandleW(NULL), NULL); SetFocus(history); break; case 42: PostMessageW(history, WM_KEYDOWN, VK_UP, 1); break; case 43: PostMessageW(history, WM_KEYDOWN, VK_UP, 1); break; case 44: PostMessageW(history, WM_KEYDOWN, VK_DOWN, 1); break; case 45: PostMessageW(history, WM_KEYDOWN, VK_DOWN, 1); break; case 46: // Rapid recalls must stay ordered, including equal-length commands. PostMessageW(history, WM_KEYDOWN, VK_UP, 1); PostMessageW(history, WM_KEYDOWN, VK_UP, 1); PostMessageW(history, WM_KEYDOWN, VK_DOWN, 1); break; case 47: // Ordinary navigation after a recall must use the refreshed snapshot. PostMessageW(history, WM_KEYDOWN, VK_LEFT, 1); PostMessageW(history, WM_KEYDOWN, VK_RIGHT, 1); break; case 48: SetWindowTextW(history, L"programmatic history sentinel"); PostMessageW(history, WM_KEYDOWN, VK_DOWN, 1); break; case 49: PostMessageW(history, WM_KEYDOWN, VK_DOWN, 1); // no history change break; case 50: // The silent worker update must still refresh the navigation snapshot. PostMessageW(history, WM_KEYDOWN, VK_RIGHT, 1); break; case 51: SendMessageW(history, EM_SETPASSWORDCHAR, L'*', 0); PostMessageW(history, WM_KEYDOWN, VK_UP, 1); break; case 52: SetFocus(password); SendMessageW(history, EM_SETPASSWORDCHAR, 0, 0); PostMessageW(history, WM_KEYDOWN, VK_UP, 1); // background recall break; default: DestroyWindow(window); break; } return 0; } if (message == WM_DESTROY) { PostQuitMessage(0); return 0; } return DefWindowProcW(window, message, wp, lp); } int WINAPI WinMain(HINSTANCE instance, HINSTANCE previous, LPSTR args, int show) { (void)previous; (void)args; (void)show; #ifdef WINE_ACCESS_WINFORMS_FIXTURE register_control(instance, L"Edit", editClass); register_control(instance, L"Static", staticClass); #endif WNDCLASSW cls = {0}; cls.hInstance = instance; cls.lpszClassName = L"WineAccessEditFixture"; cls.lpfnWndProc = window_proc; RegisterClassW(&cls); HWND window = CreateWindowW(cls.lpszClassName, L"Wine editor fixture", WS_OVERLAPPEDWINDOW, 120, 120, 500, 300, NULL, NULL, instance, NULL); editor = CreateWindowW(editClass, L"alpha beta\r\nsecond line", WS_CHILD | WS_VISIBLE | WS_TABSTOP | ES_MULTILINE | ES_AUTOVSCROLL | ES_AUTOHSCROLL, 100, 10, 350, 150, window, NULL, instance, NULL); password = CreateWindowW(editClass, L"private-password-fixture", WS_CHILD | WS_VISIBLE | ES_PASSWORD, 100, 180, 300, 30, window, NULL, instance, NULL); #ifdef WINE_ACCESS_WINFORMS_FIXTURE // Labels are created after the fields, like the observed BARD login. CreateWindowW(staticClass, L"Password:", WS_CHILD | WS_VISIBLE, 10, 183, 85, 20, window, NULL, instance, NULL); CreateWindowW(staticClass, L"Email:", WS_CHILD | WS_VISIBLE, 10, 13, 60, 20, window, NULL, instance, NULL); CreateWindowW(staticClass, L"Unrelated:", WS_CHILD | WS_VISIBLE, 10, 220, 80, 20, window, NULL, instance, NULL); #endif HMENU menu = CreateMenu(); AppendMenuW(menu, MF_STRING, 1, L"Test menu"); SetMenu(window, menu); ShowWindow(window, SW_SHOW); SetForegroundWindow(window); SetFocus(editor); SendMessageW(editor, EM_SETSEL, 0, 0); WCHAR ready[1024]; if (GetEnvironmentVariableW(L"WINE_ACCESS_FIXTURE_READY", ready, 1024)) { HANDLE file = CreateFileW(ready, GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS, 0, NULL); if (file == INVALID_HANDLE_VALUE) return 2; CloseHandle(file); } // Leave time to attach/restart a helper while this already-focused window // keeps pumping messages. Subsequent actions use the ordinary short timer. SetTimer(window, 1, 1500, NULL); MSG message; while (GetMessageW(&message, NULL, 0, 0) > 0) { if (history_input(&message)) continue; TranslateMessage(&message); DispatchMessageW(&message); } return 0; }