diff --git a/Source/WebCore/ChangeLog b/Source/WebCore/ChangeLog index 60cce98c62ad..ef8ebfc54db6 100644 --- a/Source/WebCore/ChangeLog +++ b/Source/WebCore/ChangeLog @@ -1,3 +1,16 @@ +2017-08-14 Carlos Garcia Campos + + WebDriver: handle click events on option elements + https://bugs.webkit.org/show_bug.cgi?id=174710 + + + Reviewed by Brian Burg. + + Export WebCore symbols required by WebKit layer. + + * html/HTMLOptGroupElement.h: + * html/HTMLOptionElement.h: + 2017-08-14 Chris Dumez XHR should only fire an abort event if the cancellation was requested by the client diff --git a/Source/WebCore/html/HTMLOptGroupElement.h b/Source/WebCore/html/HTMLOptGroupElement.h index 6aa19c9f3956..d42dfeb21c87 100644 --- a/Source/WebCore/html/HTMLOptGroupElement.h +++ b/Source/WebCore/html/HTMLOptGroupElement.h @@ -34,7 +34,7 @@ class HTMLOptGroupElement final : public HTMLElement { static Ref create(const QualifiedName&, Document&); bool isDisabledFormControl() const final; - HTMLSelectElement* ownerSelectElement() const; + WEBCORE_EXPORT HTMLSelectElement* ownerSelectElement() const; WEBCORE_EXPORT String groupLabelText() const; diff --git a/Source/WebCore/html/HTMLOptionElement.h b/Source/WebCore/html/HTMLOptionElement.h index 712a313034e1..25bdc8e33120 100644 --- a/Source/WebCore/html/HTMLOptionElement.h +++ b/Source/WebCore/html/HTMLOptionElement.h @@ -49,9 +49,9 @@ class HTMLOptionElement final : public HTMLElement { WEBCORE_EXPORT void setSelected(bool); #if ENABLE(DATALIST_ELEMENT) - HTMLDataListElement* ownerDataListElement() const; + WEBCORE_EXPORT HTMLDataListElement* ownerDataListElement() const; #endif - HTMLSelectElement* ownerSelectElement() const; + WEBCORE_EXPORT HTMLSelectElement* ownerSelectElement() const; WEBCORE_EXPORT String label() const; String displayLabel() const; @@ -59,7 +59,7 @@ class HTMLOptionElement final : public HTMLElement { bool ownElementDisabled() const { return m_disabled; } - bool isDisabledFormControl() const final; + WEBCORE_EXPORT bool isDisabledFormControl() const final; String textIndentedToRespectGroupLabel() const; diff --git a/Source/WebDriver/ChangeLog b/Source/WebDriver/ChangeLog index 74854d472965..a2f704f2ba96 100644 --- a/Source/WebDriver/ChangeLog +++ b/Source/WebDriver/ChangeLog @@ -1,3 +1,30 @@ +2017-08-14 Carlos Garcia Campos + + WebDriver: handle click events on option elements + https://bugs.webkit.org/show_bug.cgi?id=174710 + + + Reviewed by Brian Burg. + + Option elements are considered as a special case by the specification. When clicking an option element, we + should get its container and use it when scrolling into view and calculating in-view center point instead of the + option element itself. Then, we should not emulate a click, but change the selected status of the option element + like if it were done by a user action, firing the corresponding events. Now we check whether the element is an + option to call selectOptionElement() or performMouseInteraction(). + + This fixes more than 20 selenium tests. + + * CommandResult.cpp: + (WebDriver::CommandResult::CommandResult): Handle ElementNotSelectable protocol error. + (WebDriver::CommandResult::httpStatusCode const): Add ElementNotSelectable. + (WebDriver::CommandResult::errorString const): Ditto. + * CommandResult.h: + * Session.cpp: + (WebDriver::Session::selectOptionElement): Ask automation to select the given option element. + (WebDriver::Session::elementClick): Call selectOptionElement() or performMouseInteraction() depending on whether + the element is an option or not. + * Session.h: + 2017-08-11 Carlos Alberto Lopez Perez Fix build warning in WebDriverService.h diff --git a/Source/WebDriver/CommandResult.cpp b/Source/WebDriver/CommandResult.cpp index be8c8176a66b..a53bac979667 100644 --- a/Source/WebDriver/CommandResult.cpp +++ b/Source/WebDriver/CommandResult.cpp @@ -106,6 +106,8 @@ CommandResult::CommandResult(RefPtr&& result, std::optional&& com }); } +void Session::selectOptionElement(const String& elementID, Function&& completionHandler) +{ + RefPtr parameters = InspectorObject::create(); + parameters->setString(ASCIILiteral("browsingContextHandle"), m_toplevelBrowsingContext.value()); + parameters->setString(ASCIILiteral("frameHandle"), m_currentBrowsingContext.value()); + parameters->setString(ASCIILiteral("nodeHandle"), elementID); + m_host->sendCommandToBackend(ASCIILiteral("selectOptionElement"), WTFMove(parameters), [this, protectedThis = makeRef(*this), completionHandler = WTFMove(completionHandler)](SessionHost::CommandResponse&& response) { + if (response.isError) { + completionHandler(CommandResult::fail(WTFMove(response.responseObject))); + return; + } + completionHandler(CommandResult::success()); + }); +} + void Session::elementClick(const String& elementID, Function&& completionHandler) { if (!m_toplevelBrowsingContext) { @@ -1200,7 +1215,7 @@ void Session::elementClick(const String& elementID, Function options = ElementLayoutOption::ScrollIntoViewIfNeeded; options |= ElementLayoutOption::UseViewportCoordinates; - computeElementLayout(elementID, options, [this, protectedThis = makeRef(*this), completionHandler = WTFMove(completionHandler)](std::optional&& rect, std::optional&& inViewCenter, bool isObscured, RefPtr&& error) mutable { + computeElementLayout(elementID, options, [this, protectedThis = makeRef(*this), elementID, completionHandler = WTFMove(completionHandler)](std::optional&& rect, std::optional&& inViewCenter, bool isObscured, RefPtr&& error) mutable { if (!rect || error) { completionHandler(CommandResult::fail(WTFMove(error))); return; @@ -1214,9 +1229,27 @@ void Session::elementClick(const String& elementID, FunctionasString(tagName)) + isOptionElement = tagName == "option"; + } - waitForNavigationToComplete(WTFMove(completionHandler)); + Function continueAfterClickFunction = [this, completionHandler = WTFMove(completionHandler)](CommandResult&& result) mutable { + if (result.isError()) { + completionHandler(WTFMove(result)); + return; + } + + waitForNavigationToComplete(WTFMove(completionHandler)); + }; + if (isOptionElement) + selectOptionElement(elementID, WTFMove(continueAfterClickFunction)); + else + performMouseInteraction(inViewCenter.value().x, inViewCenter.value().y, MouseButton::Left, MouseInteraction::SingleClick, WTFMove(continueAfterClickFunction)); + }); }); } diff --git a/Source/WebDriver/Session.h b/Source/WebDriver/Session.h index 75b4ab309b9d..c3255e2c007d 100644 --- a/Source/WebDriver/Session.h +++ b/Source/WebDriver/Session.h @@ -137,6 +137,8 @@ class Session : public RefCounted { }; void computeElementLayout(const String& elementID, OptionSet, Function&&, std::optional&&, bool, RefPtr&&)>&&); + void selectOptionElement(const String& elementID, Function&&); + enum class MouseButton { None, Left, Middle, Right }; enum class MouseInteraction { Move, Down, Up, SingleClick, DoubleClick }; void performMouseInteraction(int x, int y, MouseButton, MouseInteraction, Function&&); diff --git a/Source/WebKit/ChangeLog b/Source/WebKit/ChangeLog index eb8b73a9a64f..ce6848465aa2 100644 --- a/Source/WebKit/ChangeLog +++ b/Source/WebKit/ChangeLog @@ -1,3 +1,31 @@ +2017-08-14 Carlos Garcia Campos + + WebDriver: handle click events on option elements + https://bugs.webkit.org/show_bug.cgi?id=174710 + + + Reviewed by Brian Burg. + + Add selectOptionElement method to automation to select an option element according to the WebDriver + specification. + + 14.1 Element Click. + https://w3c.github.io/webdriver/webdriver-spec.html#element-click + + * UIProcess/Automation/Automation.json: Add selectOptionElement method and ElementNotSelectable error. + * UIProcess/Automation/WebAutomationSession.cpp: + (WebKit::WebAutomationSession::selectOptionElement):Send SelectOptionElement message to the web process. + (WebKit::WebAutomationSession::didSelectOptionElement): Notify the driver. + * UIProcess/Automation/WebAutomationSession.h: + * UIProcess/Automation/WebAutomationSession.messages.in: Add DidSelectOptionElement message. + * WebProcess/Automation/WebAutomationSessionProxy.cpp: + (WebKit::elementContainer): Helper to get the container of an element according to the spec. + (WebKit::WebAutomationSessionProxy::computeElementLayout): Use the container element to scroll the view and + compute the in-view center point. + (WebKit::WebAutomationSessionProxy::selectOptionElement): Use HTMLSelectElement::optionSelectedByUser(). + * WebProcess/Automation/WebAutomationSessionProxy.h: + * WebProcess/Automation/WebAutomationSessionProxy.messages.in: Add SelectOptionElement message. + 2017-08-14 Simon Fraser Remove Proximity Events and related code diff --git a/Source/WebKit/UIProcess/Automation/Automation.json b/Source/WebKit/UIProcess/Automation/Automation.json index 21366c24e1e9..d2f929451136 100644 --- a/Source/WebKit/UIProcess/Automation/Automation.json +++ b/Source/WebKit/UIProcess/Automation/Automation.json @@ -59,7 +59,8 @@ "MissingParameter", "InvalidParameter", "InvalidSelector", - "ElementNotInteractable" + "ElementNotInteractable", + "ElementNotSelectable" ] }, { @@ -429,6 +430,16 @@ ], "async": true }, + { + "name": "selectOptionElement", + "description": "Selects the given option element. In case of container with multiple options enabled, the element selectedness is toggled.", + "parameters": [ + { "name": "browsingContextHandle", "$ref": "BrowsingContextHandle", "description": "The handle for the browsing context." }, + { "name": "frameHandle", "$ref": "FrameHandle", "description": "The handle for the frame that contains the element." }, + { "name": "nodeHandle", "$ref": "NodeHandle", "description": "The handle of the element to use." } + ], + "async": true + }, { "name": "isShowingJavaScriptDialog", "description": "Checks if a browsing context is showing a JavaScript alert, confirm, or prompt dialog.", diff --git a/Source/WebKit/UIProcess/Automation/WebAutomationSession.cpp b/Source/WebKit/UIProcess/Automation/WebAutomationSession.cpp index c8ad5a1f44ba..87f004e0dd48 100644 --- a/Source/WebKit/UIProcess/Automation/WebAutomationSession.cpp +++ b/Source/WebKit/UIProcess/Automation/WebAutomationSession.cpp @@ -845,6 +845,37 @@ void WebAutomationSession::didComputeElementLayout(uint64_t callbackID, WebCore: callback->sendSuccess(WTFMove(rectObject), WTFMove(inViewCenterPointObject), isObscured); } +void WebAutomationSession::selectOptionElement(Inspector::ErrorString& errorString, const String& browsingContextHandle, const String& frameHandle, const String& nodeHandle, Ref&& callback) +{ + WebPageProxy* page = webPageProxyForHandle(browsingContextHandle); + if (!page) + FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); + + std::optional frameID = webFrameIDForHandle(frameHandle); + if (!frameID) + FAIL_WITH_PREDEFINED_ERROR(FrameNotFound); + + uint64_t callbackID = m_nextSelectOptionElementCallbackID++; + m_selectOptionElementCallbacks.set(callbackID, WTFMove(callback)); + + page->process().send(Messages::WebAutomationSessionProxy::SelectOptionElement(page->pageID(), frameID.value(), nodeHandle, callbackID), 0); +} + +void WebAutomationSession::didSelectOptionElement(uint64_t callbackID, const String& errorType) +{ + auto callback = m_selectOptionElementCallbacks.take(callbackID); + if (!callback) + return; + + if (!errorType.isEmpty()) { + callback->sendFailure(STRING_FOR_PREDEFINED_ERROR_MESSAGE(errorType)); + return; + } + + callback->sendSuccess(); +} + + void WebAutomationSession::isShowingJavaScriptDialog(Inspector::ErrorString& errorString, const String& browsingContextHandle, bool* result) { ASSERT(m_client); diff --git a/Source/WebKit/UIProcess/Automation/WebAutomationSession.h b/Source/WebKit/UIProcess/Automation/WebAutomationSession.h index 95275c69418a..096d525eec82 100644 --- a/Source/WebKit/UIProcess/Automation/WebAutomationSession.h +++ b/Source/WebKit/UIProcess/Automation/WebAutomationSession.h @@ -133,6 +133,7 @@ class WebAutomationSession final : public API::ObjectImpl&&) override; void resolveParentFrameHandle(Inspector::ErrorString&, const String& browsingContextHandle, const String& frameHandle, Ref&&) override; void computeElementLayout(Inspector::ErrorString&, const String& browsingContextHandle, const String& frameHandle, const String& nodeHandle, const bool* optionalScrollIntoViewIfNeeded, const bool* useViewportCoordinates, Ref&&) override; + void selectOptionElement(Inspector::ErrorString&, const String& browsingContextHandle, const String& frameHandle, const String& nodeHandle, Ref&&) override; void isShowingJavaScriptDialog(Inspector::ErrorString&, const String& browsingContextHandle, bool* result) override; void dismissCurrentJavaScriptDialog(Inspector::ErrorString&, const String& browsingContextHandle) override; void acceptCurrentJavaScriptDialog(Inspector::ErrorString&, const String& browsingContextHandle) override; @@ -176,6 +177,7 @@ class WebAutomationSession final : public API::ObjectImpl, bool isObscured, const String& errorType); + void didSelectOptionElement(uint64_t callbackID, const String& errorType); void didTakeScreenshot(uint64_t callbackID, const ShareableBitmap::Handle&, const String& errorType); void didGetCookiesForFrame(uint64_t callbackID, Vector, const String& errorType); void didDeleteCookie(uint64_t callbackID, const String& errorType); @@ -241,6 +243,9 @@ class WebAutomationSession final : public API::ObjectImpl> m_deleteCookieCallbacks; + uint64_t m_nextSelectOptionElementCallbackID { 1 }; + HashMap> m_selectOptionElementCallbacks; + RunLoop::Timer m_loadTimer; Vector m_filesToSelectForFileUpload; diff --git a/Source/WebKit/UIProcess/Automation/WebAutomationSession.messages.in b/Source/WebKit/UIProcess/Automation/WebAutomationSession.messages.in index f38ac953f613..bc473b6e01e1 100644 --- a/Source/WebKit/UIProcess/Automation/WebAutomationSession.messages.in +++ b/Source/WebKit/UIProcess/Automation/WebAutomationSession.messages.in @@ -28,6 +28,8 @@ messages -> WebAutomationSession { DidComputeElementLayout(uint64_t callbackID, WebCore::IntRect rect, std::optional inViewCenterPoint, bool isObscured, String errorType) + DidSelectOptionElement(uint64_t callbackID, String errorType) + DidTakeScreenshot(uint64_t callbackID, WebKit::ShareableBitmap::Handle imageDataHandle, String errorType) DidGetCookiesForFrame(uint64_t callbackID, Vector cookies, String errorType) diff --git a/Source/WebKit/WebProcess/Automation/WebAutomationSessionProxy.cpp b/Source/WebKit/WebProcess/Automation/WebAutomationSessionProxy.cpp index 52d347d37f0f..07e4ffe69402 100644 --- a/Source/WebKit/WebProcess/Automation/WebAutomationSessionProxy.cpp +++ b/Source/WebKit/WebProcess/Automation/WebAutomationSessionProxy.cpp @@ -48,6 +48,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -503,6 +506,32 @@ static std::optional elementInViewClientCenterPoint(WebCore return clientCenterPoint; } +static WebCore::Element* containerElementForElement(WebCore::Element& element) +{ + // §13. Element State. + // https://w3c.github.io/webdriver/webdriver-spec.html#dfn-container. + if (is(element)) { + auto& optionElement = downcast(element); +#if ENABLE(DATALIST_ELEMENT) + if (auto* parentElement = optionElement.ownerDataListElement()) + return parentElement; +#endif + if (auto* parentElement = optionElement.ownerSelectElement()) + return parentElement; + + return nullptr; + } + + if (is(element)) { + if (auto* parentElement = downcast(element).ownerSelectElement()) + return parentElement; + + return nullptr; + } + + return &element; +} + void WebAutomationSessionProxy::computeElementLayout(uint64_t pageID, uint64_t frameID, String nodeHandle, bool scrollIntoViewIfNeeded, bool useViewportCoordinates, uint64_t callbackID) { WebPage* page = WebProcess::singleton().webPage(pageID); @@ -527,9 +556,12 @@ void WebAutomationSessionProxy::computeElementLayout(uint64_t pageID, uint64_t f return; } - if (scrollIntoViewIfNeeded) { + auto* containerElement = containerElementForElement(*coreElement); + if (scrollIntoViewIfNeeded && containerElement) { + // §14.1 Element Click. Step 4. Scroll into view the element’s container. + // https://w3c.github.io/webdriver/webdriver-spec.html#element-click + containerElement->scrollIntoViewIfNeeded(false); // FIXME: Wait in an implementation-specific way up to the session implicit wait timeout for the element to become in view. - coreElement->scrollIntoViewIfNeeded(false); } WebCore::IntRect rect = coreElement->clientRect(); @@ -542,15 +574,65 @@ void WebAutomationSessionProxy::computeElementLayout(uint64_t pageID, uint64_t f bool isObscured = false; std::optional inViewCenter; - if (auto clientCenterPoint = elementInViewClientCenterPoint(*coreElement, isObscured)) { - inViewCenter = WebCore::IntPoint(coreFrameView->clientToDocumentPoint(clientCenterPoint.value())); - if (useViewportCoordinates) - inViewCenter = coreFrameView->contentsToRootView(inViewCenter.value()); + if (containerElement) { + if (auto clientCenterPoint = elementInViewClientCenterPoint(*containerElement, isObscured)) { + inViewCenter = WebCore::IntPoint(coreFrameView->clientToDocumentPoint(clientCenterPoint.value())); + if (useViewportCoordinates) + inViewCenter = coreFrameView->contentsToRootView(inViewCenter.value()); + } } WebProcess::singleton().parentProcessConnection()->send(Messages::WebAutomationSession::DidComputeElementLayout(callbackID, rect, inViewCenter, isObscured, String()), 0); } +void WebAutomationSessionProxy::selectOptionElement(uint64_t pageID, uint64_t frameID, String nodeHandle, uint64_t callbackID) +{ + WebPage* page = WebProcess::singleton().webPage(pageID); + if (!page) { + String windowNotFoundErrorType = Inspector::Protocol::AutomationHelpers::getEnumConstantValue(Inspector::Protocol::Automation::ErrorMessage::WindowNotFound); + WebProcess::singleton().parentProcessConnection()->send(Messages::WebAutomationSession::DidSelectOptionElement(callbackID, windowNotFoundErrorType), 0); + return; + } + + WebFrame* frame = frameID ? WebProcess::singleton().webFrame(frameID) : page->mainWebFrame(); + if (!frame || !frame->coreFrame() || !frame->coreFrame()->view()) { + String frameNotFoundErrorType = Inspector::Protocol::AutomationHelpers::getEnumConstantValue(Inspector::Protocol::Automation::ErrorMessage::FrameNotFound); + WebProcess::singleton().parentProcessConnection()->send(Messages::WebAutomationSession::DidSelectOptionElement(callbackID, frameNotFoundErrorType), 0); + return; + } + + WebCore::Element* coreElement = elementForNodeHandle(*frame, nodeHandle); + if (!coreElement || (!is(coreElement) && !is(coreElement))) { + String nodeNotFoundErrorType = Inspector::Protocol::AutomationHelpers::getEnumConstantValue(Inspector::Protocol::Automation::ErrorMessage::NodeNotFound); + WebProcess::singleton().parentProcessConnection()->send(Messages::WebAutomationSession::DidSelectOptionElement(callbackID, nodeNotFoundErrorType), 0); + return; + } + + String elementNotInteractableErrorType = Inspector::Protocol::AutomationHelpers::getEnumConstantValue(Inspector::Protocol::Automation::ErrorMessage::ElementNotInteractable); + if (is(coreElement)) { + WebProcess::singleton().parentProcessConnection()->send(Messages::WebAutomationSession::DidSelectOptionElement(callbackID, elementNotInteractableErrorType), 0); + return; + } + + auto& optionElement = downcast(*coreElement); + auto* selectElement = optionElement.ownerSelectElement(); + if (!selectElement) { + WebProcess::singleton().parentProcessConnection()->send(Messages::WebAutomationSession::DidSelectOptionElement(callbackID, elementNotInteractableErrorType), 0); + return; + } + + if (selectElement->isDisabledFormControl() || optionElement.isDisabledFormControl()) { + String elementNotSelectableErrorType = Inspector::Protocol::AutomationHelpers::getEnumConstantValue(Inspector::Protocol::Automation::ErrorMessage::ElementNotSelectable); + WebProcess::singleton().parentProcessConnection()->send(Messages::WebAutomationSession::DidSelectOptionElement(callbackID, elementNotSelectableErrorType), 0); + return; + } + + // FIXME: According to the spec we should fire mouse over, move and down events, then input and change, and finally mouse up and click. + // optionSelectedByUser() will fire input and change events if needed, but all other events should be fired manually here. + selectElement->optionSelectedByUser(optionElement.index(), true, selectElement->multiple()); + WebProcess::singleton().parentProcessConnection()->send(Messages::WebAutomationSession::DidSelectOptionElement(callbackID, { }), 0); +} + void WebAutomationSessionProxy::takeScreenshot(uint64_t pageID, uint64_t callbackID) { ShareableBitmap::Handle handle; diff --git a/Source/WebKit/WebProcess/Automation/WebAutomationSessionProxy.h b/Source/WebKit/WebProcess/Automation/WebAutomationSessionProxy.h index 4b979443ace9..845674bd431b 100644 --- a/Source/WebKit/WebProcess/Automation/WebAutomationSessionProxy.h +++ b/Source/WebKit/WebProcess/Automation/WebAutomationSessionProxy.h @@ -65,6 +65,7 @@ class WebAutomationSessionProxy : public IPC::MessageReceiver { void resolveParentFrame(uint64_t pageID, uint64_t frameID, uint64_t callbackID); void focusFrame(uint64_t pageID, uint64_t frameID); void computeElementLayout(uint64_t pageID, uint64_t frameID, String nodeHandle, bool scrollIntoViewIfNeeded, bool useViewportCoordinates, uint64_t callbackID); + void selectOptionElement(uint64_t pageID, uint64_t frameID, String nodeHandle, uint64_t callbackID); void takeScreenshot(uint64_t pageID, uint64_t callbackID); void getCookiesForFrame(uint64_t pageID, uint64_t frameID, uint64_t callbackID); void deleteCookie(uint64_t pageID, uint64_t frameID, String cookieName, uint64_t callbackID); diff --git a/Source/WebKit/WebProcess/Automation/WebAutomationSessionProxy.messages.in b/Source/WebKit/WebProcess/Automation/WebAutomationSessionProxy.messages.in index 2daf2a5d5631..9d4131c85fa2 100644 --- a/Source/WebKit/WebProcess/Automation/WebAutomationSessionProxy.messages.in +++ b/Source/WebKit/WebProcess/Automation/WebAutomationSessionProxy.messages.in @@ -32,6 +32,8 @@ messages -> WebAutomationSessionProxy { ComputeElementLayout(uint64_t pageID, uint64_t frameID, String nodeHandle, bool scrollIntoViewIfNeeded, bool useViewportCoordinates, uint64_t callbackID) + SelectOptionElement(uint64_t pageID, uint64_t frameID, String nodeHandle, uint64_t callbackID) + TakeScreenshot(uint64_t pageID, uint64_t callbackID) GetCookiesForFrame(uint64_t pageID, uint64_t frameID, uint64_t callbackID)