I have a c method that pops up a WPF dialog. The c method wraps a call of our .net library and gets the dialog output (c cli). The dialog is essentially a login dialog, asking for username and password. I want to test in c that I get the right dialog output when the dialog receives some username and password input.
In essence, my test goes as follows:
struct ShowCredentialsDialogContext
{
ShowCredentialsDialogContext(const std::function<Credentials()>& fnc)
: _fnc(fnc)
{ }
void Handle()
{
Creds = _fnc();
}
Credentials Creds;
private:
std::function<Credentials()> _fnc;
};
TEST_F(DialogsFixture, UsernamePasswordDialogReturnsEnteredCredentials)
{
// Arrange
ShowCredentialsDialogContext ctx(&MyLib::ShowUsernamePasswordDialog);
const auto& functionOnThread = std::bind(&ShowCredentialsDialogContext::Handle, &ctx);
std::thread thread(functionOnThread);
InitializeHandleToWindowWithTitle(L"Authentication required");
const std::wstring& expectedUsername = L"ųƢȝɬᴥ";
const std::wstring& expectedPassword = L"ỗỷⓩ✟ᵺ";
// Act
SendUnicodeInput(expectedUsername);
GoToNextControl();
SendUnicodeInput(expectedPassword);
ValidateDialog();
thread.join();
// Assert
ASSERT_EQ(expectedUsername, ctx.Creds.Username);
ASSERT_EQ(expectedPassword, ctx.Creds.Password);
}
where
class DialogsFixture : public BaseFixture
{
public:
DialogsFixture()
: _handle(nullptr)
{}
protected:
void InitializeHandleToWindowWithTitle(const std::wstring& title, int nbRetries = 10, int timeStepInMs = 100)
{
while (nbRetries >= 0)
{
if (const auto handle = FindWindowW(0, title.c_str()))
{
_handle = handle;
return;
}
--nbRetries;
std::this_thread::sleep_for(std::chrono::milliseconds(timeStepInMs));
}
throw TimeOutException("Unable to get handle to window");
}
void SendUnicodeInput(const std::wstring& msg) const
{
for (const auto ch : msg)
{
if (!PostMessageW(_handle, WM_CHAR, ch, NULL))
{
FAIL() << "got error " << GetLastError() << std::endl;
}
}
}
void GoToNextControl() const
{
SendControlInput(VK_TAB);
}
void ValidateDialog() const
{
SendControlInput(VK_RETURN);
}
void CancelDialog() const
{
SendControlInput(VK_ESCAPE);
}
private:
void SendControlInput(WORD ctrl) const
{
if (!PostMessageW(_handle, WM_KEYDOWN, ctrl, NULL))
{
FAIL() << "got error " << GetLastError() << std::endl;
}
}
private:
HWND _handle;
};
I chose to proceed with PostMessage instead of SendInput because the latter doesn't fill my dialog on my Team City agent while the former fills it as expected (I have other tests with simpler dialogs that are perfectly green).
My problem is now that the password is not set in the dialog after
SendUnicodeInput(expectedUsername);
GoToNextControl();
SendUnicodeInput(expectedPassword);
The username gets perfectly displayed, then the caret gets placed to the password input field, but then nothing happens. The password does not get displayed in the password field. I have another dialog containing only a password field and my test of that dialog works flawlessly. I can fill the password input field without problem. As soon as I send the VK_TAB any character I post afterwards does not get displayed. The same holds if I replace VK_TAB with VK_SPACE for example.
I am a bit clueless here, I have tried a lot of stuff, nothing seems to work. What am I doing wrong here?
Replacing the DialogsFixture with this makes it perfectly work on my local development machine:
class DialogsFixture : public BaseFixture
{
public:
DialogsFixture()
: _handle(nullptr)
{}
protected:
void InitializeHandleToWindowWithTitle(const std::wstring& title, int nbRetries = 10, int timeStepInMs = 100)
{
// same as before
[...]
}
void SendUnicodeInput(const std::wstring& msg) const
{
SetForegroundWindow(_handle);
std::vector<INPUT> vec;
for (const auto ch : msg)
{
INPUT input = { 0 };
input.type = INPUT_KEYBOARD;
input.ki.dwFlags = KEYEVENTF_UNICODE;
input.ki.wScan = ch;
vec.push_back(input);
input.ki.dwFlags |= KEYEVENTF_KEYUP;
vec.push_back(input);
}
SendInput(vec.size(), vec.data(), sizeof INPUT);
}
// same as before
[...]
private:
void SendControlInput(WORD ctrl) const
{
SetForegroundWindow(_handle);
INPUT ip;
ip.type = INPUT_KEYBOARD;
ip.ki.time = 0;
ip.ki.dwFlags = 0;
ip.ki.wScan = 0;
ip.ki.wVk = ctrl;
ip.ki.dwExtraInfo = 0;
SendInput(1, &ip, sizeof INPUT);
ip.ki.dwFlags = KEYEVENTF_KEYUP;
SendInput(1, &ip, sizeof INPUT);
}
private:
HWND _handle;
};
Unfortunately, that doesn't work on my team city agent. It seems like the dialog receives no keyboard input, nothing gets displayed on it. With the PostMessage variant, the dialog receives data. How can I make my test work?
CodePudding user response:
Not directly addressing the problem, but I think you can sidestep it: You send PostMessage to the dialog, and rely that you are in the right spot. That works fine in demo programs, but is tricky to handle in automated tests. If something goes wrong and you are not in the right spot you will still send the message somewhere, and need luck to figure out that there is a problem in the first place, and more luck to understand what exactly goes wrong. Windows tends to swallow messages.
You can sidestep this if you use EnumChildWindows instead to figure out the window handles of the controls in your dialog. Then you can send PostMessage directly to the controls. https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-enumchildwindows
I'm not sure if this works well in WPF, I've done similar GUI tests in MFC and VB.
CodePudding user response:
From the comments I received and after some googling, I came up to the conclusion that indeed the PostMessage approach isn't ideal in general. However, in my particular case, because everything I need is just to fill two input strings with a few chars for the sake of very basic testing, I decided to modify my GoToNextControl method as follows:
void GoToNextControl() const
{
SendControlInput(VK_TAB);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
It is in general not recommended to apply such hacks in tests because it can make them flaky. However, in my particular case, after several hundreds of successful runs, I accept this as the quick'n'easy solution to my problem. It is by no means a general solution. The general, reliable solution would involve the ui automation, as suggested by the comments.
