// Copyright 2011 Software Freedom Conservancy | |
// Licensed under the Apache License, Version 2.0 (the "License"); | |
// you may not use this file except in compliance with the License. | |
// You may obtain a copy of the License at | |
// | |
// http://www.apache.org/licenses/LICENSE-2.0 | |
// | |
// Unless required by applicable law or agreed to in writing, software | |
// distributed under the License is distributed on an "AS IS" BASIS, | |
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
// See the License for the specific language governing permissions and | |
// limitations under the License. | |
#ifndef WEBDRIVER_IE_SENDKEYSCOMMANDHANDLER_H_ | |
#define WEBDRIVER_IE_SENDKEYSCOMMANDHANDLER_H_ | |
#include <ctime> | |
#include "../Browser.h" | |
#include "../IECommandHandler.h" | |
#include "../IECommandExecutor.h" | |
#include "interactions.h" | |
#include "logging.h" | |
const LPCTSTR fileDialogNames[] = { | |
_T("#32770"), | |
_T("ComboBoxEx32"), | |
_T("ComboBox"), | |
_T("Edit"), | |
NULL | |
}; | |
namespace webdriver { | |
class SendKeysCommandHandler : public IECommandHandler { | |
public: | |
struct FileNameData { | |
HWND main; | |
HWND hwnd; | |
DWORD ieProcId; | |
const wchar_t* text; | |
}; | |
SendKeysCommandHandler(void) { | |
} | |
virtual ~SendKeysCommandHandler(void) { | |
} | |
protected: | |
void ExecuteInternal(const IECommandExecutor& executor, | |
const LocatorMap& locator_parameters, | |
const ParametersMap& command_parameters, | |
Response* response) | |
{ | |
LocatorMap::const_iterator id_parameter_iterator = locator_parameters.find("id"); | |
ParametersMap::const_iterator value_parameter_iterator = command_parameters.find("value"); | |
if (id_parameter_iterator == locator_parameters.end()) { | |
response->SetErrorResponse(400, "Missing parameter in URL: id"); | |
return; | |
} else if (value_parameter_iterator == command_parameters.end()) { | |
response->SetErrorResponse(400, "Missing parameter: value"); | |
return; | |
} else { | |
std::string element_id = id_parameter_iterator->second; | |
Json::Value key_array = value_parameter_iterator->second; | |
BrowserHandle browser_wrapper; | |
int status_code = executor.GetCurrentBrowser(&browser_wrapper); | |
if (status_code != SUCCESS) { | |
response->SetErrorResponse(status_code, "Unable to get browser"); | |
return; | |
} | |
HWND window_handle = browser_wrapper->GetWindowHandle(); | |
ElementHandle element_wrapper; | |
status_code = this->GetElement(executor, element_id, &element_wrapper); | |
if (status_code == SUCCESS) { | |
bool displayed; | |
status_code = element_wrapper->IsDisplayed(&displayed); | |
if (status_code != SUCCESS || !displayed) { | |
response->SetErrorResponse(EELEMENTNOTDISPLAYED, | |
"Element is not displayed"); | |
return; | |
} | |
if (!element_wrapper->IsEnabled()) { | |
response->SetErrorResponse(EELEMENTNOTENABLED, | |
"Element is not enabled"); | |
return; | |
} | |
CComQIPtr<IHTMLElement> element(element_wrapper->element()); | |
LocationInfo location = {}; | |
element_wrapper->GetLocationOnceScrolledIntoView(executor.input_manager()->scroll_behavior(), &location); | |
CComQIPtr<IHTMLInputFileElement> file(element); | |
CComQIPtr<IHTMLInputElement> input(element); | |
CComBSTR element_type; | |
if (input) { | |
input->get_type(&element_type); | |
element_type.ToLower(); | |
} | |
bool is_file_element = (file != NULL) || | |
(input != NULL && element_type == L"file"); | |
if (is_file_element) { | |
std::wstring keys = L""; | |
for (unsigned int i = 0; i < key_array.size(); ++i ) { | |
std::string key(key_array[i].asString()); | |
keys.append(CA2W(key.c_str(), CP_UTF8)); | |
} | |
DWORD ie_process_id; | |
::GetWindowThreadProcessId(window_handle, &ie_process_id); | |
HWND top_level_window_handle = browser_wrapper->GetTopLevelWindowHandle(); | |
FileNameData key_data; | |
key_data.main = top_level_window_handle; | |
key_data.hwnd = window_handle; | |
key_data.text = keys.c_str(); | |
key_data.ieProcId = ie_process_id; | |
unsigned int thread_id; | |
HANDLE thread_handle = reinterpret_cast<HANDLE>(_beginthreadex(NULL, | |
0, | |
&SendKeysCommandHandler::SetFileValue, | |
reinterpret_cast<void*>(&key_data), | |
0, | |
&thread_id)); | |
element->click(); | |
// We're now blocked until the dialog closes. | |
::CloseHandle(thread_handle); | |
return; | |
} | |
this->VerifyPageHasFocus(browser_wrapper->GetTopLevelWindowHandle(), browser_wrapper->window_handle()); | |
this->WaitUntilElementFocused(element); | |
std::string null_character = CW2A(L"\uE000", CP_UTF8); | |
Json::Value value = this->RecreateJsonParameterObject(command_parameters); | |
value["value"].append(null_character); | |
value["action"] = "keys"; | |
Json::UInt index = 0; | |
Json::Value actions(Json::arrayValue); | |
actions[index] = value; | |
status_code = executor.input_manager()->PerformInputSequence(browser_wrapper, actions); | |
//status_code = executor.input_manager()->SendKeystrokes(browser_wrapper, key_array, true); | |
response->SetSuccessResponse(Json::Value::null); | |
return; | |
} else { | |
response->SetErrorResponse(status_code, "Element is no longer valid"); | |
return; | |
} | |
} | |
} | |
private: | |
static unsigned int WINAPI SetFileValue(void *file_data) { | |
FileNameData* data = reinterpret_cast<FileNameData*>(file_data); | |
::Sleep(100); | |
HWND ie_main_window_handle = data->main; | |
HWND dialog_window_handle = ::GetLastActivePopup(ie_main_window_handle); | |
int max_wait = 10; | |
while ((dialog_window_handle == ie_main_window_handle) && --max_wait) { | |
::Sleep(100); | |
dialog_window_handle = ::GetLastActivePopup(ie_main_window_handle); | |
} | |
if (!dialog_window_handle || | |
(dialog_window_handle == ie_main_window_handle)) { | |
LOG(WARN) << "No dialog directly owned by the top-level window"; | |
// No dialog directly owned by the top-level window. | |
// Look for a dialog belonging to the same process as | |
// the IE server window. This isn't perfect, but it's | |
// all we have for now. | |
max_wait = 50; | |
while ((dialog_window_handle == ie_main_window_handle) && --max_wait) { | |
::Sleep(100); | |
ProcessWindowInfo process_win_info; | |
process_win_info.dwProcessId = data->ieProcId; | |
::EnumWindows(&BrowserFactory::FindDialogWindowForProcess, | |
reinterpret_cast<LPARAM>(&process_win_info)); | |
if (process_win_info.hwndBrowser != NULL) { | |
dialog_window_handle = process_win_info.hwndBrowser; | |
} | |
} | |
} | |
if (!dialog_window_handle || | |
(dialog_window_handle == ie_main_window_handle)) { | |
LOG(WARN) << "No dialog found"; | |
return false; | |
} | |
return SendKeysToFileUploadAlert(dialog_window_handle, data->text); | |
} | |
static bool SendKeysToFileUploadAlert(HWND dialog_window_handle, | |
const wchar_t* value) { | |
HWND edit_field_window_handle = NULL; | |
int maxWait = 10; | |
while (!edit_field_window_handle && --maxWait) { | |
wait(200); | |
edit_field_window_handle = dialog_window_handle; | |
for (int i = 1; fileDialogNames[i]; ++i) { | |
edit_field_window_handle = getChildWindow(edit_field_window_handle, | |
fileDialogNames[i]); | |
} | |
} | |
if (edit_field_window_handle) { | |
// Attempt to set the value, looping until we succeed. | |
const wchar_t* filename = value; | |
size_t expected = wcslen(filename); | |
size_t curr = 0; | |
while (expected != curr) { | |
::SendMessage(edit_field_window_handle, | |
WM_SETTEXT, | |
0, | |
reinterpret_cast<LPARAM>(filename)); | |
wait(1000); | |
curr = ::SendMessage(edit_field_window_handle, WM_GETTEXTLENGTH, 0, 0); | |
} | |
bool triedToDismiss = false; | |
for (int i = 0; i < 10000; i++) { | |
HWND open_window_handle = ::GetDlgItem(dialog_window_handle, IDOK); | |
if (open_window_handle) { | |
LRESULT total = 0; | |
total += ::SendMessage(open_window_handle, WM_LBUTTONDOWN, 0, 0); | |
total += ::SendMessage(open_window_handle, WM_LBUTTONUP, 0, 0); | |
if (total == 0) { | |
triedToDismiss = true; | |
// Sometimes IE10 doesn't dismiss this dialog after the messages | |
// are received, even though the messages were processed | |
// successfully. If not, try again, just in case. | |
if (!IsWindow(dialog_window_handle)) { | |
return true; | |
} | |
} | |
wait(200); | |
} else if (triedToDismiss) { | |
// Probably just a slow close | |
return true; | |
} | |
} | |
LOG(ERROR) << "Unable to set value of file input dialog"; | |
return false; | |
} | |
LOG(WARN) << "No edit found"; | |
return false; | |
} | |
void VerifyPageHasFocus(HWND top_level_window_handle, HWND browser_pane_window_handle) { | |
DWORD proc; | |
DWORD thread_id = ::GetWindowThreadProcessId(top_level_window_handle, &proc); | |
GUITHREADINFO info; | |
info.cbSize = sizeof(GUITHREADINFO); | |
::GetGUIThreadInfo(thread_id, &info); | |
if (info.hwndFocus != browser_pane_window_handle) { | |
// The focus is on a UI element other than the HTML viewer pane (like | |
// the address bar, for instance). This has implications for certain | |
// keystrokes, like backspace. We need to set the focus to the HTML | |
// viewer pane. | |
// N.B. The SetFocus() API should *NOT* cause the IE browser window to | |
// magically appear in the foreground. If that is not true, we will need | |
// to find some other solution. | |
LOG(DEBUG) << "Focus is on a UI element other than the HTML viewer pane."; | |
DWORD current_thread_id = ::GetCurrentThreadId(); | |
::AttachThreadInput(current_thread_id, thread_id, TRUE); | |
::SetFocus(browser_pane_window_handle); | |
::AttachThreadInput(current_thread_id, thread_id, FALSE); | |
} | |
} | |
bool WaitUntilElementFocused(IHTMLElement *element) { | |
// Check we have focused the element. | |
bool has_focus = false; | |
CComPtr<IDispatch> dispatch; | |
element->get_document(&dispatch); | |
CComQIPtr<IHTMLDocument2> document(dispatch); | |
// If the element we want is already the focused element, we're done. | |
CComPtr<IHTMLElement> active_element; | |
if (document->get_activeElement(&active_element) == S_OK) { | |
if (active_element.IsEqualObject(element)) { | |
return true; | |
} | |
} | |
CComQIPtr<IHTMLElement2> element2(element); | |
element2->focus(); | |
clock_t max_wait = clock() + 1000; | |
for (int i = clock(); i < max_wait; i = clock()) { | |
wait(1); | |
CComPtr<IHTMLElement> active_wait_element; | |
if (document->get_activeElement(&active_wait_element) == S_OK) { | |
CComQIPtr<IHTMLElement2> active_wait_element2(active_wait_element); | |
if (element2.IsEqualObject(active_wait_element2)) { | |
this->SetInsertionPoint(element); | |
has_focus = true; | |
break; | |
} | |
} | |
} | |
if (!has_focus) { | |
LOG(WARN) << "We don't have focus on element."; | |
} | |
return has_focus; | |
} | |
bool SetInsertionPoint(IHTMLElement* element) { | |
CComPtr<IHTMLTxtRange> range; | |
CComQIPtr<IHTMLInputTextElement> input_element(element); | |
if (input_element) { | |
input_element->createTextRange(&range); | |
} else { | |
CComQIPtr<IHTMLTextAreaElement> text_area_element(element); | |
if (text_area_element) { | |
text_area_element->createTextRange(&range); | |
} | |
} | |
if (range) { | |
range->collapse(VARIANT_FALSE); | |
range->select(); | |
return true; | |
} | |
return false; | |
} | |
}; | |
} // namespace webdriver | |
#endif // WEBDRIVER_IE_SENDKEYSCOMMANDHANDLER_H_ |