| 1 | |
| 2 | /* |
| 3 | * Copyright (C) 2016, 2017 Apple Inc. All rights reserved. |
| 4 | * |
| 5 | * Redistribution and use in source and binary forms, with or without |
| 6 | * modification, are permitted provided that the following conditions |
| 7 | * are met: |
| 8 | * 1. Redistributions of source code must retain the above copyright |
| 9 | * notice, this list of conditions and the following disclaimer. |
| 10 | * 2. Redistributions in binary form must reproduce the above copyright |
| 11 | * notice, this list of conditions and the following disclaimer in the |
| 12 | * documentation and/or other materials provided with the distribution. |
| 13 | * |
| 14 | * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
| 15 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
| 16 | * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| 17 | * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
| 18 | * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| 19 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| 20 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| 21 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| 22 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| 23 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
| 24 | * THE POSSIBILITY OF SUCH DAMAGE. |
| 25 | */ |
| 26 | |
| 27 | #include "config.h" |
| 28 | #include "WebAutomationSession.h" |
| 29 | |
| 30 | #include "APIArray.h" |
| 31 | #include "APIAutomationSessionClient.h" |
| 32 | #include "APINavigation.h" |
| 33 | #include "APIOpenPanelParameters.h" |
| 34 | #include "AutomationProtocolObjects.h" |
| 35 | #include "CoordinateSystem.h" |
| 36 | #include "WebAutomationSessionMacros.h" |
| 37 | #include "WebAutomationSessionMessages.h" |
| 38 | #include "WebAutomationSessionProxyMessages.h" |
| 39 | #include "WebCookieManagerProxy.h" |
| 40 | #include "WebFullScreenManagerProxy.h" |
| 41 | #include "WebInspectorProxy.h" |
| 42 | #include "WebOpenPanelResultListenerProxy.h" |
| 43 | #include "WebProcessPool.h" |
| 44 | #include <JavaScriptCore/InspectorBackendDispatcher.h> |
| 45 | #include <JavaScriptCore/InspectorFrontendRouter.h> |
| 46 | #include <WebCore/MIMETypeRegistry.h> |
| 47 | #include <algorithm> |
| 48 | #include <wtf/HashMap.h> |
| 49 | #include <wtf/Optional.h> |
| 50 | #include <wtf/URL.h> |
| 51 | #include <wtf/UUID.h> |
| 52 | #include <wtf/text/StringConcatenate.h> |
| 53 | |
| 54 | namespace WebKit { |
| 55 | |
| 56 | using namespace Inspector; |
| 57 | |
| 58 | String AutomationCommandError::toProtocolString() |
| 59 | { |
| 60 | String protocolErrorName = Inspector::Protocol::AutomationHelpers::getEnumConstantValue(type); |
| 61 | if (!message.hasValue()) |
| 62 | return protocolErrorName; |
| 63 | |
| 64 | return makeString(protocolErrorName, errorNameAndDetailsSeparator, message.value()); |
| 65 | } |
| 66 | |
| 67 | // §8. Sessions |
| 68 | // https://www.w3.org/TR/webdriver/#dfn-session-page-load-timeout |
| 69 | static const Seconds defaultPageLoadTimeout = 300_s; |
| 70 | // https://www.w3.org/TR/webdriver/#dfn-page-loading-strategy |
| 71 | static const Inspector::Protocol::Automation::PageLoadStrategy defaultPageLoadStrategy = Inspector::Protocol::Automation::PageLoadStrategy::Normal; |
| 72 | |
| 73 | WebAutomationSession::WebAutomationSession() |
| 74 | : m_client(std::make_unique<API::AutomationSessionClient>()) |
| 75 | , m_frontendRouter(FrontendRouter::create()) |
| 76 | , m_backendDispatcher(BackendDispatcher::create(m_frontendRouter.copyRef())) |
| 77 | , m_domainDispatcher(AutomationBackendDispatcher::create(m_backendDispatcher, this)) |
| 78 | , m_domainNotifier(std::make_unique<AutomationFrontendDispatcher>(m_frontendRouter)) |
| 79 | , m_loadTimer(RunLoop::main(), this, &WebAutomationSession::loadTimerFired) |
| 80 | { |
| 81 | #if ENABLE(WEBDRIVER_ACTIONS_API) |
| 82 | // Set up canonical input sources to be used for 'performInteractionSequence' and 'cancelInteractionSequence'. |
| 83 | #if ENABLE(WEBDRIVER_TOUCH_INTERACTIONS) |
| 84 | m_inputSources.add(SimulatedInputSource::create(SimulatedInputSourceType::Touch)); |
| 85 | #endif |
| 86 | #if ENABLE(WEBDRIVER_MOUSE_INTERACTIONS) |
| 87 | m_inputSources.add(SimulatedInputSource::create(SimulatedInputSourceType::Mouse)); |
| 88 | #endif |
| 89 | #if ENABLE(WEBDRIVER_KEYBOARD_INTERACTIONS) |
| 90 | m_inputSources.add(SimulatedInputSource::create(SimulatedInputSourceType::Keyboard)); |
| 91 | #endif |
| 92 | m_inputSources.add(SimulatedInputSource::create(SimulatedInputSourceType::Null)); |
| 93 | #endif // ENABLE(WEBDRIVER_ACTIONS_API) |
| 94 | } |
| 95 | |
| 96 | WebAutomationSession::~WebAutomationSession() |
| 97 | { |
| 98 | ASSERT(!m_client); |
| 99 | ASSERT(!m_processPool); |
| 100 | } |
| 101 | |
| 102 | void WebAutomationSession::setClient(std::unique_ptr<API::AutomationSessionClient>&& client) |
| 103 | { |
| 104 | m_client = WTFMove(client); |
| 105 | } |
| 106 | |
| 107 | void WebAutomationSession::setProcessPool(WebKit::WebProcessPool* processPool) |
| 108 | { |
| 109 | if (m_processPool) |
| 110 | m_processPool->removeMessageReceiver(Messages::WebAutomationSession::messageReceiverName()); |
| 111 | |
| 112 | m_processPool = processPool; |
| 113 | |
| 114 | if (m_processPool) |
| 115 | m_processPool->addMessageReceiver(Messages::WebAutomationSession::messageReceiverName(), *this); |
| 116 | } |
| 117 | |
| 118 | // NOTE: this class could be split at some point to support local and remote automation sessions. |
| 119 | // For now, it only works with a remote automation driver over a RemoteInspector connection. |
| 120 | |
| 121 | #if ENABLE(REMOTE_INSPECTOR) |
| 122 | |
| 123 | // Inspector::RemoteAutomationTarget API |
| 124 | |
| 125 | void WebAutomationSession::dispatchMessageFromRemote(const String& message) |
| 126 | { |
| 127 | m_backendDispatcher->dispatch(message); |
| 128 | } |
| 129 | |
| 130 | void WebAutomationSession::connect(Inspector::FrontendChannel& channel, bool isAutomaticConnection, bool immediatelyPause) |
| 131 | { |
| 132 | UNUSED_PARAM(isAutomaticConnection); |
| 133 | UNUSED_PARAM(immediatelyPause); |
| 134 | |
| 135 | m_remoteChannel = &channel; |
| 136 | m_frontendRouter->connectFrontend(channel); |
| 137 | |
| 138 | setIsPaired(true); |
| 139 | } |
| 140 | |
| 141 | void WebAutomationSession::disconnect(Inspector::FrontendChannel& channel) |
| 142 | { |
| 143 | ASSERT(&channel == m_remoteChannel); |
| 144 | terminate(); |
| 145 | } |
| 146 | |
| 147 | #endif // ENABLE(REMOTE_INSPECTOR) |
| 148 | |
| 149 | void WebAutomationSession::terminate() |
| 150 | { |
| 151 | #if ENABLE(WEBDRIVER_KEYBOARD_INTERACTIONS) |
| 152 | for (auto& identifier : copyToVector(m_pendingKeyboardEventsFlushedCallbacksPerPage.keys())) { |
| 153 | auto callback = m_pendingKeyboardEventsFlushedCallbacksPerPage.take(identifier); |
| 154 | callback(AUTOMATION_COMMAND_ERROR_WITH_NAME(InternalError)); |
| 155 | } |
| 156 | #endif // ENABLE(WEBDRIVER_KEYBOARD_INTERACTIONS) |
| 157 | |
| 158 | #if ENABLE(WEBDRIVER_MOUSE_INTERACTIONS) |
| 159 | for (auto& identifier : copyToVector(m_pendingMouseEventsFlushedCallbacksPerPage.keys())) { |
| 160 | auto callback = m_pendingMouseEventsFlushedCallbacksPerPage.take(identifier); |
| 161 | callback(AUTOMATION_COMMAND_ERROR_WITH_NAME(InternalError)); |
| 162 | } |
| 163 | #endif // ENABLE(WEBDRIVER_MOUSE_INTERACTIONS) |
| 164 | |
| 165 | #if ENABLE(REMOTE_INSPECTOR) |
| 166 | if (Inspector::FrontendChannel* channel = m_remoteChannel) { |
| 167 | m_remoteChannel = nullptr; |
| 168 | m_frontendRouter->disconnectFrontend(*channel); |
| 169 | } |
| 170 | |
| 171 | setIsPaired(false); |
| 172 | #endif |
| 173 | |
| 174 | if (m_client) |
| 175 | m_client->didDisconnectFromRemote(*this); |
| 176 | } |
| 177 | |
| 178 | WebPageProxy* WebAutomationSession::webPageProxyForHandle(const String& handle) |
| 179 | { |
| 180 | auto iter = m_handleWebPageMap.find(handle); |
| 181 | if (iter == m_handleWebPageMap.end()) |
| 182 | return nullptr; |
| 183 | return WebProcessProxy::webPage(iter->value); |
| 184 | } |
| 185 | |
| 186 | String WebAutomationSession::handleForWebPageProxy(const WebPageProxy& webPageProxy) |
| 187 | { |
| 188 | auto iter = m_webPageHandleMap.find(webPageProxy.pageID()); |
| 189 | if (iter != m_webPageHandleMap.end()) |
| 190 | return iter->value; |
| 191 | |
| 192 | String handle = "page-" + createCanonicalUUIDString().convertToASCIIUppercase(); |
| 193 | |
| 194 | auto firstAddResult = m_webPageHandleMap.add(webPageProxy.pageID(), handle); |
| 195 | RELEASE_ASSERT(firstAddResult.isNewEntry); |
| 196 | |
| 197 | auto secondAddResult = m_handleWebPageMap.add(handle, webPageProxy.pageID()); |
| 198 | RELEASE_ASSERT(secondAddResult.isNewEntry); |
| 199 | |
| 200 | return handle; |
| 201 | } |
| 202 | |
| 203 | Optional<uint64_t> WebAutomationSession::webFrameIDForHandle(const String& handle) |
| 204 | { |
| 205 | if (handle.isEmpty()) |
| 206 | return 0; |
| 207 | |
| 208 | auto iter = m_handleWebFrameMap.find(handle); |
| 209 | if (iter == m_handleWebFrameMap.end()) |
| 210 | return WTF::nullopt; |
| 211 | |
| 212 | return iter->value; |
| 213 | } |
| 214 | |
| 215 | String WebAutomationSession::handleForWebFrameID(uint64_t frameID) |
| 216 | { |
| 217 | if (!frameID) |
| 218 | return emptyString(); |
| 219 | |
| 220 | for (auto& process : m_processPool->processes()) { |
| 221 | if (WebFrameProxy* frame = process->webFrame(frameID)) { |
| 222 | if (frame->isMainFrame()) |
| 223 | return emptyString(); |
| 224 | break; |
| 225 | } |
| 226 | } |
| 227 | |
| 228 | auto iter = m_webFrameHandleMap.find(frameID); |
| 229 | if (iter != m_webFrameHandleMap.end()) |
| 230 | return iter->value; |
| 231 | |
| 232 | String handle = "frame-" + createCanonicalUUIDString().convertToASCIIUppercase(); |
| 233 | |
| 234 | auto firstAddResult = m_webFrameHandleMap.add(frameID, handle); |
| 235 | RELEASE_ASSERT(firstAddResult.isNewEntry); |
| 236 | |
| 237 | auto secondAddResult = m_handleWebFrameMap.add(handle, frameID); |
| 238 | RELEASE_ASSERT(secondAddResult.isNewEntry); |
| 239 | |
| 240 | return handle; |
| 241 | } |
| 242 | |
| 243 | String WebAutomationSession::handleForWebFrameProxy(const WebFrameProxy& webFrameProxy) |
| 244 | { |
| 245 | return handleForWebFrameID(webFrameProxy.frameID()); |
| 246 | } |
| 247 | |
| 248 | Ref<Inspector::Protocol::Automation::BrowsingContext> WebAutomationSession::buildBrowsingContextForPage(WebPageProxy& page, WebCore::FloatRect windowFrame) |
| 249 | { |
| 250 | auto originObject = Inspector::Protocol::Automation::Point::create() |
| 251 | .setX(windowFrame.x()) |
| 252 | .setY(windowFrame.y()) |
| 253 | .release(); |
| 254 | |
| 255 | auto sizeObject = Inspector::Protocol::Automation::Size::create() |
| 256 | .setWidth(windowFrame.width()) |
| 257 | .setHeight(windowFrame.height()) |
| 258 | .release(); |
| 259 | |
| 260 | bool isActive = page.isViewVisible() && page.isViewFocused() && page.isViewWindowActive(); |
| 261 | String handle = handleForWebPageProxy(page); |
| 262 | |
| 263 | return Inspector::Protocol::Automation::BrowsingContext::create() |
| 264 | .setHandle(handle) |
| 265 | .setActive(isActive) |
| 266 | .setUrl(page.pageLoadState().activeURL()) |
| 267 | .setWindowOrigin(WTFMove(originObject)) |
| 268 | .setWindowSize(WTFMove(sizeObject)) |
| 269 | .release(); |
| 270 | } |
| 271 | |
| 272 | // Platform-independent Commands. |
| 273 | |
| 274 | void WebAutomationSession::getNextContext(Ref<WebAutomationSession>&& protectedThis, Vector<Ref<WebPageProxy>>&& pages, Ref<JSON::ArrayOf<Inspector::Protocol::Automation::BrowsingContext>> contexts, Ref<WebAutomationSession::GetBrowsingContextsCallback>&& callback) |
| 275 | { |
| 276 | if (pages.isEmpty()) { |
| 277 | callback->sendSuccess(WTFMove(contexts)); |
| 278 | return; |
| 279 | } |
| 280 | auto page = pages.takeLast(); |
| 281 | auto& webPageProxy = page.get(); |
| 282 | webPageProxy.getWindowFrameWithCallback([this, protectedThis = WTFMove(protectedThis), callback = WTFMove(callback), pages = WTFMove(pages), contexts = WTFMove(contexts), page = WTFMove(page)](WebCore::FloatRect windowFrame) mutable { |
| 283 | contexts->addItem(protectedThis->buildBrowsingContextForPage(page.get(), windowFrame)); |
| 284 | getNextContext(WTFMove(protectedThis), WTFMove(pages), WTFMove(contexts), WTFMove(callback)); |
| 285 | }); |
| 286 | } |
| 287 | |
| 288 | void WebAutomationSession::getBrowsingContexts(Ref<GetBrowsingContextsCallback>&& callback) |
| 289 | { |
| 290 | Vector<Ref<WebPageProxy>> pages; |
| 291 | for (auto& process : m_processPool->processes()) { |
| 292 | for (auto* page : process->pages()) { |
| 293 | ASSERT(page); |
| 294 | if (!page->isControlledByAutomation()) |
| 295 | continue; |
| 296 | pages.append(*page); |
| 297 | } |
| 298 | } |
| 299 | |
| 300 | getNextContext(makeRef(*this), WTFMove(pages), JSON::ArrayOf<Inspector::Protocol::Automation::BrowsingContext>::create(), WTFMove(callback)); |
| 301 | } |
| 302 | |
| 303 | void WebAutomationSession::getBrowsingContext(const String& handle, Ref<GetBrowsingContextCallback>&& callback) |
| 304 | { |
| 305 | WebPageProxy* page = webPageProxyForHandle(handle); |
| 306 | if (!page) |
| 307 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
| 308 | |
| 309 | page->getWindowFrameWithCallback([protectedThis = makeRef(*this), page = makeRef(*page), callback = WTFMove(callback)](WebCore::FloatRect windowFrame) mutable { |
| 310 | callback->sendSuccess(protectedThis->buildBrowsingContextForPage(page.get(), windowFrame)); |
| 311 | }); |
| 312 | } |
| 313 | |
| 314 | static Inspector::Protocol::Automation::BrowsingContextPresentation toProtocol(API::AutomationSessionClient::BrowsingContextPresentation value) |
| 315 | { |
| 316 | switch (value) { |
| 317 | case API::AutomationSessionClient::BrowsingContextPresentation::Tab: |
| 318 | return Inspector::Protocol::Automation::BrowsingContextPresentation::Tab; |
| 319 | case API::AutomationSessionClient::BrowsingContextPresentation::Window: |
| 320 | return Inspector::Protocol::Automation::BrowsingContextPresentation::Window; |
| 321 | } |
| 322 | |
| 323 | RELEASE_ASSERT_NOT_REACHED(); |
| 324 | } |
| 325 | |
| 326 | void WebAutomationSession::createBrowsingContext(const String* optionalPresentationHint, Ref<CreateBrowsingContextCallback>&& callback) |
| 327 | { |
| 328 | ASSERT(m_client); |
| 329 | if (!m_client) |
| 330 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InternalError, "The remote session could not request a new browsing context." ); |
| 331 | |
| 332 | uint16_t options = 0; |
| 333 | |
| 334 | if (optionalPresentationHint) { |
| 335 | auto parsedPresentationHint = Inspector::Protocol::AutomationHelpers::parseEnumValueFromString<Inspector::Protocol::Automation::BrowsingContextPresentation>(*optionalPresentationHint); |
| 336 | if (parsedPresentationHint.hasValue() && parsedPresentationHint.value() == Inspector::Protocol::Automation::BrowsingContextPresentation::Tab) |
| 337 | options |= API::AutomationSessionBrowsingContextOptionsPreferNewTab; |
| 338 | } |
| 339 | |
| 340 | m_client->requestNewPageWithOptions(*this, static_cast<API::AutomationSessionBrowsingContextOptions>(options), [protectedThis = makeRef(*this), callback = WTFMove(callback)](WebPageProxy* page) { |
| 341 | if (!page) |
| 342 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InternalError, "The remote session failed to create a new browsing context." ); |
| 343 | |
| 344 | callback->sendSuccess(protectedThis->handleForWebPageProxy(*page), toProtocol(protectedThis->m_client->currentPresentationOfPage(protectedThis.get(), *page))); |
| 345 | }); |
| 346 | } |
| 347 | |
| 348 | void WebAutomationSession::closeBrowsingContext(Inspector::ErrorString& errorString, const String& handle) |
| 349 | { |
| 350 | WebPageProxy* page = webPageProxyForHandle(handle); |
| 351 | if (!page) |
| 352 | SYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
| 353 | |
| 354 | page->closePage(false); |
| 355 | } |
| 356 | |
| 357 | void WebAutomationSession::switchToBrowsingContext(const String& browsingContextHandle, const String* optionalFrameHandle, Ref<SwitchToBrowsingContextCallback>&& callback) |
| 358 | { |
| 359 | WebPageProxy* page = webPageProxyForHandle(browsingContextHandle); |
| 360 | if (!page) |
| 361 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
| 362 | |
| 363 | Optional<uint64_t> frameID = webFrameIDForHandle(optionalFrameHandle ? *optionalFrameHandle : emptyString()); |
| 364 | if (!frameID) |
| 365 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(FrameNotFound); |
| 366 | |
| 367 | |
| 368 | m_client->requestSwitchToPage(*this, *page, [frameID, page = makeRef(*page), callback = WTFMove(callback)]() { |
| 369 | page->setFocus(true); |
| 370 | page->process().send(Messages::WebAutomationSessionProxy::FocusFrame(page->pageID(), frameID.value()), 0); |
| 371 | |
| 372 | callback->sendSuccess(); |
| 373 | }); |
| 374 | } |
| 375 | |
| 376 | void WebAutomationSession::setWindowFrameOfBrowsingContext(const String& handle, const JSON::Object* optionalOriginObject, const JSON::Object* optionalSizeObject, Ref<SetWindowFrameOfBrowsingContextCallback>&& callback) |
| 377 | { |
| 378 | Optional<float> x; |
| 379 | Optional<float> y; |
| 380 | if (optionalOriginObject) { |
| 381 | if (!(x = optionalOriginObject->getNumber<float>("x"_s ))) |
| 382 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(MissingParameter, "The 'x' parameter was not found or invalid." ); |
| 383 | |
| 384 | if (!(y = optionalOriginObject->getNumber<float>("y"_s ))) |
| 385 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(MissingParameter, "The 'y' parameter was not found or invalid." ); |
| 386 | |
| 387 | if (x.value() < 0) |
| 388 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "The 'x' parameter had an invalid value." ); |
| 389 | |
| 390 | if (y.value() < 0) |
| 391 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "The 'y' parameter had an invalid value." ); |
| 392 | } |
| 393 | |
| 394 | Optional<float> width; |
| 395 | Optional<float> height; |
| 396 | if (optionalSizeObject) { |
| 397 | if (!(width = optionalSizeObject->getNumber<float>("width"_s ))) |
| 398 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(MissingParameter, "The 'width' parameter was not found or invalid." ); |
| 399 | |
| 400 | if (!(height = optionalSizeObject->getNumber<float>("height"_s ))) |
| 401 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(MissingParameter, "The 'height' parameter was not found or invalid." ); |
| 402 | |
| 403 | if (width.value() < 0) |
| 404 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "The 'width' parameter had an invalid value." ); |
| 405 | |
| 406 | if (height.value() < 0) |
| 407 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "The 'height' parameter had an invalid value." ); |
| 408 | } |
| 409 | |
| 410 | WebPageProxy* page = webPageProxyForHandle(handle); |
| 411 | if (!page) |
| 412 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
| 413 | |
| 414 | exitFullscreenWindowForPage(*page, [this, protectedThis = makeRef(*this), callback = WTFMove(callback), page = makeRefPtr(page), width, height, x, y]() mutable { |
| 415 | auto& webPage = *page; |
| 416 | this->restoreWindowForPage(webPage, [callback = WTFMove(callback), page = WTFMove(page), width, height, x, y]() mutable { |
| 417 | auto& webPage = *page; |
| 418 | webPage.getWindowFrameWithCallback([callback = WTFMove(callback), page = WTFMove(page), width, height, x, y](WebCore::FloatRect originalFrame) mutable { |
| 419 | WebCore::FloatRect newFrame = WebCore::FloatRect(WebCore::FloatPoint(x.valueOr(originalFrame.location().x()), y.valueOr(originalFrame.location().y())), WebCore::FloatSize(width.valueOr(originalFrame.size().width()), height.valueOr(originalFrame.size().height()))); |
| 420 | if (newFrame != originalFrame) |
| 421 | page->setWindowFrame(newFrame); |
| 422 | |
| 423 | callback->sendSuccess(); |
| 424 | }); |
| 425 | }); |
| 426 | }); |
| 427 | } |
| 428 | |
| 429 | static Optional<Inspector::Protocol::Automation::PageLoadStrategy> pageLoadStrategyFromStringParameter(const String* optionalPageLoadStrategyString) |
| 430 | { |
| 431 | if (!optionalPageLoadStrategyString) |
| 432 | return defaultPageLoadStrategy; |
| 433 | |
| 434 | auto parsedPageLoadStrategy = Inspector::Protocol::AutomationHelpers::parseEnumValueFromString<Inspector::Protocol::Automation::PageLoadStrategy>(*optionalPageLoadStrategyString); |
| 435 | if (!parsedPageLoadStrategy) |
| 436 | return WTF::nullopt; |
| 437 | return parsedPageLoadStrategy; |
| 438 | } |
| 439 | |
| 440 | void WebAutomationSession::waitForNavigationToComplete(const String& browsingContextHandle, const String* optionalFrameHandle, const String* optionalPageLoadStrategyString, const int* optionalPageLoadTimeout, Ref<WaitForNavigationToCompleteCallback>&& callback) |
| 441 | { |
| 442 | WebPageProxy* page = webPageProxyForHandle(browsingContextHandle); |
| 443 | if (!page) |
| 444 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
| 445 | |
| 446 | auto pageLoadStrategy = pageLoadStrategyFromStringParameter(optionalPageLoadStrategyString); |
| 447 | if (!pageLoadStrategy) |
| 448 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "The parameter 'pageLoadStrategy' is invalid." ); |
| 449 | auto pageLoadTimeout = optionalPageLoadTimeout ? Seconds::fromMilliseconds(*optionalPageLoadTimeout) : defaultPageLoadTimeout; |
| 450 | |
| 451 | // If page is loading and there's an active JavaScript dialog is probably because the |
| 452 | // dialog was started in an onload handler, so in case of normal page load strategy the |
| 453 | // load will not finish until the dialog is dismissed. Instead of waiting for the timeout, |
| 454 | // we return without waiting since we know it will timeout for sure. We want to check |
| 455 | // arguments first, though. |
| 456 | bool shouldTimeoutDueToUnexpectedAlert = pageLoadStrategy.value() == Inspector::Protocol::Automation::PageLoadStrategy::Normal |
| 457 | && page->pageLoadState().isLoading() && m_client->isShowingJavaScriptDialogOnPage(*this, *page); |
| 458 | |
| 459 | if (optionalFrameHandle && !optionalFrameHandle->isEmpty()) { |
| 460 | Optional<uint64_t> frameID = webFrameIDForHandle(*optionalFrameHandle); |
| 461 | if (!frameID) |
| 462 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(FrameNotFound); |
| 463 | WebFrameProxy* frame = page->process().webFrame(frameID.value()); |
| 464 | if (!frame) |
| 465 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(FrameNotFound); |
| 466 | if (!shouldTimeoutDueToUnexpectedAlert) |
| 467 | waitForNavigationToCompleteOnFrame(*frame, pageLoadStrategy.value(), pageLoadTimeout, WTFMove(callback)); |
| 468 | } else { |
| 469 | if (!shouldTimeoutDueToUnexpectedAlert) |
| 470 | waitForNavigationToCompleteOnPage(*page, pageLoadStrategy.value(), pageLoadTimeout, WTFMove(callback)); |
| 471 | } |
| 472 | |
| 473 | if (shouldTimeoutDueToUnexpectedAlert) { |
| 474 | // §9 Navigation. |
| 475 | // 7. If the previous step completed by the session page load timeout being reached and the browser does not |
| 476 | // have an active user prompt, return error with error code timeout. |
| 477 | // 8. Return success with data null. |
| 478 | // https://w3c.github.io/webdriver/webdriver-spec.html#dfn-wait-for-navigation-to-complete |
| 479 | callback->sendSuccess(); |
| 480 | } |
| 481 | } |
| 482 | |
| 483 | void WebAutomationSession::waitForNavigationToCompleteOnPage(WebPageProxy& page, Inspector::Protocol::Automation::PageLoadStrategy loadStrategy, Seconds timeout, Ref<Inspector::BackendDispatcher::CallbackBase>&& callback) |
| 484 | { |
| 485 | ASSERT(!m_loadTimer.isActive()); |
| 486 | if (loadStrategy == Inspector::Protocol::Automation::PageLoadStrategy::None || !page.pageLoadState().isLoading()) { |
| 487 | callback->sendSuccess(JSON::Object::create()); |
| 488 | return; |
| 489 | } |
| 490 | |
| 491 | m_loadTimer.startOneShot(timeout); |
| 492 | switch (loadStrategy) { |
| 493 | case Inspector::Protocol::Automation::PageLoadStrategy::Normal: |
| 494 | m_pendingNormalNavigationInBrowsingContextCallbacksPerPage.set(page.pageID(), WTFMove(callback)); |
| 495 | break; |
| 496 | case Inspector::Protocol::Automation::PageLoadStrategy::Eager: |
| 497 | m_pendingEagerNavigationInBrowsingContextCallbacksPerPage.set(page.pageID(), WTFMove(callback)); |
| 498 | break; |
| 499 | case Inspector::Protocol::Automation::PageLoadStrategy::None: |
| 500 | ASSERT_NOT_REACHED(); |
| 501 | } |
| 502 | } |
| 503 | |
| 504 | void WebAutomationSession::waitForNavigationToCompleteOnFrame(WebFrameProxy& frame, Inspector::Protocol::Automation::PageLoadStrategy loadStrategy, Seconds timeout, Ref<Inspector::BackendDispatcher::CallbackBase>&& callback) |
| 505 | { |
| 506 | ASSERT(!m_loadTimer.isActive()); |
| 507 | if (loadStrategy == Inspector::Protocol::Automation::PageLoadStrategy::None || frame.frameLoadState().state() == FrameLoadState::State::Finished) { |
| 508 | callback->sendSuccess(JSON::Object::create()); |
| 509 | return; |
| 510 | } |
| 511 | |
| 512 | m_loadTimer.startOneShot(timeout); |
| 513 | switch (loadStrategy) { |
| 514 | case Inspector::Protocol::Automation::PageLoadStrategy::Normal: |
| 515 | m_pendingNormalNavigationInBrowsingContextCallbacksPerFrame.set(frame.frameID(), WTFMove(callback)); |
| 516 | break; |
| 517 | case Inspector::Protocol::Automation::PageLoadStrategy::Eager: |
| 518 | m_pendingEagerNavigationInBrowsingContextCallbacksPerFrame.set(frame.frameID(), WTFMove(callback)); |
| 519 | break; |
| 520 | case Inspector::Protocol::Automation::PageLoadStrategy::None: |
| 521 | ASSERT_NOT_REACHED(); |
| 522 | } |
| 523 | } |
| 524 | |
| 525 | void WebAutomationSession::respondToPendingPageNavigationCallbacksWithTimeout(HashMap<uint64_t, RefPtr<Inspector::BackendDispatcher::CallbackBase>>& map) |
| 526 | { |
| 527 | Inspector::ErrorString timeoutError = STRING_FOR_PREDEFINED_ERROR_NAME(Timeout); |
| 528 | for (auto id : copyToVector(map.keys())) { |
| 529 | auto page = WebProcessProxy::webPage(id); |
| 530 | auto callback = map.take(id); |
| 531 | if (page && m_client->isShowingJavaScriptDialogOnPage(*this, *page)) |
| 532 | callback->sendSuccess(JSON::Object::create()); |
| 533 | else |
| 534 | callback->sendFailure(timeoutError); |
| 535 | } |
| 536 | } |
| 537 | |
| 538 | static WebPageProxy* findPageForFrameID(const WebProcessPool& processPool, uint64_t frameID) |
| 539 | { |
| 540 | for (auto& process : processPool.processes()) { |
| 541 | if (auto* frame = process->webFrame(frameID)) |
| 542 | return frame->page(); |
| 543 | } |
| 544 | return nullptr; |
| 545 | } |
| 546 | |
| 547 | void WebAutomationSession::respondToPendingFrameNavigationCallbacksWithTimeout(HashMap<uint64_t, RefPtr<Inspector::BackendDispatcher::CallbackBase>>& map) |
| 548 | { |
| 549 | Inspector::ErrorString timeoutError = STRING_FOR_PREDEFINED_ERROR_NAME(Timeout); |
| 550 | for (auto id : copyToVector(map.keys())) { |
| 551 | auto* page = findPageForFrameID(*m_processPool, id); |
| 552 | auto callback = map.take(id); |
| 553 | if (page && m_client->isShowingJavaScriptDialogOnPage(*this, *page)) |
| 554 | callback->sendSuccess(JSON::Object::create()); |
| 555 | else |
| 556 | callback->sendFailure(timeoutError); |
| 557 | } |
| 558 | } |
| 559 | |
| 560 | void WebAutomationSession::loadTimerFired() |
| 561 | { |
| 562 | respondToPendingFrameNavigationCallbacksWithTimeout(m_pendingNormalNavigationInBrowsingContextCallbacksPerFrame); |
| 563 | respondToPendingFrameNavigationCallbacksWithTimeout(m_pendingEagerNavigationInBrowsingContextCallbacksPerFrame); |
| 564 | respondToPendingPageNavigationCallbacksWithTimeout(m_pendingNormalNavigationInBrowsingContextCallbacksPerPage); |
| 565 | respondToPendingPageNavigationCallbacksWithTimeout(m_pendingEagerNavigationInBrowsingContextCallbacksPerPage); |
| 566 | } |
| 567 | |
| 568 | void WebAutomationSession::maximizeWindowOfBrowsingContext(const String& browsingContextHandle, Ref<MaximizeWindowOfBrowsingContextCallback>&& callback) |
| 569 | { |
| 570 | auto* page = webPageProxyForHandle(browsingContextHandle); |
| 571 | if (!page) |
| 572 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
| 573 | |
| 574 | exitFullscreenWindowForPage(*page, [this, protectedThis = makeRef(*this), callback = WTFMove(callback), page = makeRefPtr(page)]() mutable { |
| 575 | auto& webPage = *page; |
| 576 | restoreWindowForPage(webPage, [this, callback = WTFMove(callback), page = WTFMove(page)]() mutable { |
| 577 | maximizeWindowForPage(*page, [callback = WTFMove(callback)]() { |
| 578 | callback->sendSuccess(); |
| 579 | }); |
| 580 | }); |
| 581 | }); |
| 582 | } |
| 583 | |
| 584 | void WebAutomationSession::hideWindowOfBrowsingContext(const String& browsingContextHandle, Ref<HideWindowOfBrowsingContextCallback>&& callback) |
| 585 | { |
| 586 | WebPageProxy* page = webPageProxyForHandle(browsingContextHandle); |
| 587 | if (!page) |
| 588 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
| 589 | |
| 590 | exitFullscreenWindowForPage(*page, [protectedThis = makeRef(*this), callback = WTFMove(callback), page = makeRefPtr(page)]() mutable { |
| 591 | protectedThis->hideWindowForPage(*page, [callback = WTFMove(callback)]() mutable { |
| 592 | callback->sendSuccess(); |
| 593 | }); |
| 594 | }); |
| 595 | } |
| 596 | |
| 597 | void WebAutomationSession::exitFullscreenWindowForPage(WebPageProxy& page, WTF::CompletionHandler<void()>&& completionHandler) |
| 598 | { |
| 599 | #if ENABLE(FULLSCREEN_API) |
| 600 | ASSERT(!m_windowStateTransitionCallback); |
| 601 | if (!page.fullScreenManager() || !page.fullScreenManager()->isFullScreen()) { |
| 602 | completionHandler(); |
| 603 | return; |
| 604 | } |
| 605 | |
| 606 | m_windowStateTransitionCallback = WTF::Function<void(WindowTransitionedToState)> { [this, protectedThis = makeRef(*this), completionHandler = WTFMove(completionHandler)](WindowTransitionedToState state) mutable { |
| 607 | // If fullscreen exited and we didn't request that, just ignore it. |
| 608 | if (state != WindowTransitionedToState::Unfullscreen) |
| 609 | return; |
| 610 | |
| 611 | // Keep this callback in scope so completionHandler does not get destroyed before we call it. |
| 612 | auto protectedCallback = WTFMove(m_windowStateTransitionCallback); |
| 613 | completionHandler(); |
| 614 | } }; |
| 615 | |
| 616 | page.fullScreenManager()->requestExitFullScreen(); |
| 617 | #else |
| 618 | completionHandler(); |
| 619 | #endif |
| 620 | } |
| 621 | |
| 622 | void WebAutomationSession::restoreWindowForPage(WebPageProxy& page, WTF::CompletionHandler<void()>&& completionHandler) |
| 623 | { |
| 624 | m_client->requestRestoreWindowOfPage(*this, page, WTFMove(completionHandler)); |
| 625 | } |
| 626 | |
| 627 | void WebAutomationSession::maximizeWindowForPage(WebPageProxy& page, WTF::CompletionHandler<void()>&& completionHandler) |
| 628 | { |
| 629 | m_client->requestMaximizeWindowOfPage(*this, page, WTFMove(completionHandler)); |
| 630 | } |
| 631 | |
| 632 | void WebAutomationSession::hideWindowForPage(WebPageProxy& page, WTF::CompletionHandler<void()>&& completionHandler) |
| 633 | { |
| 634 | m_client->requestHideWindowOfPage(*this, page, WTFMove(completionHandler)); |
| 635 | } |
| 636 | |
| 637 | void WebAutomationSession::willShowJavaScriptDialog(WebPageProxy& page) |
| 638 | { |
| 639 | // Wait until the next run loop iteration to give time for the client to show the dialog, |
| 640 | // then check if the dialog is still present. If the page is loading, the dialog will block |
| 641 | // the load in case of normal strategy, so we want to dispatch all pending navigation callbacks. |
| 642 | // If the dialog was shown during a script execution, we want to finish the evaluateJavaScriptFunction |
| 643 | // operation with an unexpected alert open error. |
| 644 | RunLoop::main().dispatch([this, protectedThis = makeRef(*this), page = makeRef(page)] { |
| 645 | if (!page->hasRunningProcess() || !m_client || !m_client->isShowingJavaScriptDialogOnPage(*this, page)) |
| 646 | return; |
| 647 | |
| 648 | if (page->pageLoadState().isLoading()) { |
| 649 | m_loadTimer.stop(); |
| 650 | respondToPendingFrameNavigationCallbacksWithTimeout(m_pendingNormalNavigationInBrowsingContextCallbacksPerFrame); |
| 651 | respondToPendingPageNavigationCallbacksWithTimeout(m_pendingNormalNavigationInBrowsingContextCallbacksPerPage); |
| 652 | } |
| 653 | |
| 654 | if (!m_evaluateJavaScriptFunctionCallbacks.isEmpty()) { |
| 655 | for (auto key : copyToVector(m_evaluateJavaScriptFunctionCallbacks.keys())) { |
| 656 | auto callback = m_evaluateJavaScriptFunctionCallbacks.take(key); |
| 657 | callback->sendSuccess("null"_s ); |
| 658 | } |
| 659 | } |
| 660 | |
| 661 | #if ENABLE(WEBDRIVER_MOUSE_INTERACTIONS) |
| 662 | if (!m_pendingMouseEventsFlushedCallbacksPerPage.isEmpty()) { |
| 663 | for (auto key : copyToVector(m_pendingMouseEventsFlushedCallbacksPerPage.keys())) { |
| 664 | auto callback = m_pendingMouseEventsFlushedCallbacksPerPage.take(key); |
| 665 | callback(WTF::nullopt); |
| 666 | } |
| 667 | } |
| 668 | #endif // ENABLE(WEBDRIVER_MOUSE_INTERACTIONS) |
| 669 | |
| 670 | #if ENABLE(WEBDRIVER_KEYBOARD_INTERACTIONS) |
| 671 | if (!m_pendingKeyboardEventsFlushedCallbacksPerPage.isEmpty()) { |
| 672 | for (auto key : copyToVector(m_pendingKeyboardEventsFlushedCallbacksPerPage.keys())) { |
| 673 | auto callback = m_pendingKeyboardEventsFlushedCallbacksPerPage.take(key); |
| 674 | callback(WTF::nullopt); |
| 675 | } |
| 676 | } |
| 677 | #endif // ENABLE(WEBDRIVER_KEYBOARD_INTERACTIONS) |
| 678 | }); |
| 679 | } |
| 680 | |
| 681 | void WebAutomationSession::didEnterFullScreenForPage(const WebPageProxy&) |
| 682 | { |
| 683 | if (m_windowStateTransitionCallback) |
| 684 | m_windowStateTransitionCallback(WindowTransitionedToState::Fullscreen); |
| 685 | } |
| 686 | |
| 687 | void WebAutomationSession::didExitFullScreenForPage(const WebPageProxy&) |
| 688 | { |
| 689 | if (m_windowStateTransitionCallback) |
| 690 | m_windowStateTransitionCallback(WindowTransitionedToState::Unfullscreen); |
| 691 | } |
| 692 | |
| 693 | void WebAutomationSession::navigateBrowsingContext(const String& handle, const String& url, const String* optionalPageLoadStrategyString, const int* optionalPageLoadTimeout, Ref<NavigateBrowsingContextCallback>&& callback) |
| 694 | { |
| 695 | WebPageProxy* page = webPageProxyForHandle(handle); |
| 696 | if (!page) |
| 697 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
| 698 | |
| 699 | auto pageLoadStrategy = pageLoadStrategyFromStringParameter(optionalPageLoadStrategyString); |
| 700 | if (!pageLoadStrategy) |
| 701 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "The parameter 'pageLoadStrategy' is invalid." ); |
| 702 | auto pageLoadTimeout = optionalPageLoadTimeout ? Seconds::fromMilliseconds(*optionalPageLoadTimeout) : defaultPageLoadTimeout; |
| 703 | |
| 704 | page->loadRequest(URL(URL(), url)); |
| 705 | waitForNavigationToCompleteOnPage(*page, pageLoadStrategy.value(), pageLoadTimeout, WTFMove(callback)); |
| 706 | } |
| 707 | |
| 708 | void WebAutomationSession::goBackInBrowsingContext(const String& handle, const String* optionalPageLoadStrategyString, const int* optionalPageLoadTimeout, Ref<GoBackInBrowsingContextCallback>&& callback) |
| 709 | { |
| 710 | WebPageProxy* page = webPageProxyForHandle(handle); |
| 711 | if (!page) |
| 712 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
| 713 | |
| 714 | auto pageLoadStrategy = pageLoadStrategyFromStringParameter(optionalPageLoadStrategyString); |
| 715 | if (!pageLoadStrategy) |
| 716 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "The parameter 'pageLoadStrategy' is invalid." ); |
| 717 | auto pageLoadTimeout = optionalPageLoadTimeout ? Seconds::fromMilliseconds(*optionalPageLoadTimeout) : defaultPageLoadTimeout; |
| 718 | |
| 719 | page->goBack(); |
| 720 | waitForNavigationToCompleteOnPage(*page, pageLoadStrategy.value(), pageLoadTimeout, WTFMove(callback)); |
| 721 | } |
| 722 | |
| 723 | void WebAutomationSession::goForwardInBrowsingContext(const String& handle, const String* optionalPageLoadStrategyString, const int* optionalPageLoadTimeout, Ref<GoForwardInBrowsingContextCallback>&& callback) |
| 724 | { |
| 725 | WebPageProxy* page = webPageProxyForHandle(handle); |
| 726 | if (!page) |
| 727 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
| 728 | |
| 729 | auto pageLoadStrategy = pageLoadStrategyFromStringParameter(optionalPageLoadStrategyString); |
| 730 | if (!pageLoadStrategy) |
| 731 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "The parameter 'pageLoadStrategy' is invalid." ); |
| 732 | auto pageLoadTimeout = optionalPageLoadTimeout ? Seconds::fromMilliseconds(*optionalPageLoadTimeout) : defaultPageLoadTimeout; |
| 733 | |
| 734 | page->goForward(); |
| 735 | waitForNavigationToCompleteOnPage(*page, pageLoadStrategy.value(), pageLoadTimeout, WTFMove(callback)); |
| 736 | } |
| 737 | |
| 738 | void WebAutomationSession::reloadBrowsingContext(const String& handle, const String* optionalPageLoadStrategyString, const int* optionalPageLoadTimeout, Ref<ReloadBrowsingContextCallback>&& callback) |
| 739 | { |
| 740 | WebPageProxy* page = webPageProxyForHandle(handle); |
| 741 | if (!page) |
| 742 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
| 743 | |
| 744 | auto pageLoadStrategy = pageLoadStrategyFromStringParameter(optionalPageLoadStrategyString); |
| 745 | if (!pageLoadStrategy) |
| 746 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "The parameter 'pageLoadStrategy' is invalid." ); |
| 747 | auto pageLoadTimeout = optionalPageLoadTimeout ? Seconds::fromMilliseconds(*optionalPageLoadTimeout) : defaultPageLoadTimeout; |
| 748 | |
| 749 | page->reload({ }); |
| 750 | waitForNavigationToCompleteOnPage(*page, pageLoadStrategy.value(), pageLoadTimeout, WTFMove(callback)); |
| 751 | } |
| 752 | |
| 753 | void WebAutomationSession::navigationOccurredForFrame(const WebFrameProxy& frame) |
| 754 | { |
| 755 | if (frame.isMainFrame()) { |
| 756 | // New page loaded, clear frame handles previously cached. |
| 757 | m_handleWebFrameMap.clear(); |
| 758 | m_webFrameHandleMap.clear(); |
| 759 | if (auto callback = m_pendingNormalNavigationInBrowsingContextCallbacksPerPage.take(frame.page()->pageID())) { |
| 760 | m_loadTimer.stop(); |
| 761 | callback->sendSuccess(JSON::Object::create()); |
| 762 | } |
| 763 | m_domainNotifier->browsingContextCleared(handleForWebPageProxy(*frame.page())); |
| 764 | } else { |
| 765 | if (auto callback = m_pendingNormalNavigationInBrowsingContextCallbacksPerFrame.take(frame.frameID())) { |
| 766 | m_loadTimer.stop(); |
| 767 | callback->sendSuccess(JSON::Object::create()); |
| 768 | } |
| 769 | } |
| 770 | } |
| 771 | |
| 772 | void WebAutomationSession::documentLoadedForFrame(const WebFrameProxy& frame) |
| 773 | { |
| 774 | if (frame.isMainFrame()) { |
| 775 | if (auto callback = m_pendingEagerNavigationInBrowsingContextCallbacksPerPage.take(frame.page()->pageID())) { |
| 776 | m_loadTimer.stop(); |
| 777 | callback->sendSuccess(JSON::Object::create()); |
| 778 | } |
| 779 | } else { |
| 780 | if (auto callback = m_pendingEagerNavigationInBrowsingContextCallbacksPerFrame.take(frame.frameID())) { |
| 781 | m_loadTimer.stop(); |
| 782 | callback->sendSuccess(JSON::Object::create()); |
| 783 | } |
| 784 | } |
| 785 | } |
| 786 | |
| 787 | void WebAutomationSession::inspectorFrontendLoaded(const WebPageProxy& page) |
| 788 | { |
| 789 | if (auto callback = m_pendingInspectorCallbacksPerPage.take(page.pageID())) |
| 790 | callback->sendSuccess(JSON::Object::create()); |
| 791 | } |
| 792 | |
| 793 | void WebAutomationSession::mouseEventsFlushedForPage(const WebPageProxy& page) |
| 794 | { |
| 795 | #if ENABLE(WEBDRIVER_MOUSE_INTERACTIONS) |
| 796 | if (auto callback = m_pendingMouseEventsFlushedCallbacksPerPage.take(page.pageID())) |
| 797 | callback(WTF::nullopt); |
| 798 | #else |
| 799 | UNUSED_PARAM(page); |
| 800 | #endif |
| 801 | } |
| 802 | |
| 803 | void WebAutomationSession::keyboardEventsFlushedForPage(const WebPageProxy& page) |
| 804 | { |
| 805 | #if ENABLE(WEBDRIVER_KEYBOARD_INTERACTIONS) |
| 806 | if (auto callback = m_pendingKeyboardEventsFlushedCallbacksPerPage.take(page.pageID())) |
| 807 | callback(WTF::nullopt); |
| 808 | #else |
| 809 | UNUSED_PARAM(page); |
| 810 | #endif |
| 811 | } |
| 812 | |
| 813 | void WebAutomationSession::willClosePage(const WebPageProxy& page) |
| 814 | { |
| 815 | String handle = handleForWebPageProxy(page); |
| 816 | m_domainNotifier->browsingContextCleared(handle); |
| 817 | |
| 818 | // Cancel pending interactions on this page. By providing an error, this will cause subsequent |
| 819 | // actions to be aborted and the SimulatedInputDispatcher::run() call will unwind and fail. |
| 820 | #if ENABLE(WEBDRIVER_MOUSE_INTERACTIONS) |
| 821 | if (auto callback = m_pendingMouseEventsFlushedCallbacksPerPage.take(page.pageID())) |
| 822 | callback(AUTOMATION_COMMAND_ERROR_WITH_NAME(WindowNotFound)); |
| 823 | #endif |
| 824 | #if ENABLE(WEBDRIVER_KEYBOARD_INTERACTIONS) |
| 825 | if (auto callback = m_pendingKeyboardEventsFlushedCallbacksPerPage.take(page.pageID())) |
| 826 | callback(AUTOMATION_COMMAND_ERROR_WITH_NAME(WindowNotFound)); |
| 827 | #endif |
| 828 | |
| 829 | #if ENABLE(WEBDRIVER_ACTIONS_API) |
| 830 | // Then tell the input dispatcher to cancel so timers are stopped, and let it go out of scope. |
| 831 | Optional<Ref<SimulatedInputDispatcher>> inputDispatcher = m_inputDispatchersByPage.take(page.pageID()); |
| 832 | if (inputDispatcher.hasValue()) |
| 833 | inputDispatcher.value()->cancel(); |
| 834 | #endif |
| 835 | } |
| 836 | |
| 837 | static bool fileCanBeAcceptedForUpload(const String& filename, const HashSet<String>& allowedMIMETypes, const HashSet<String>& allowedFileExtensions) |
| 838 | { |
| 839 | if (!FileSystem::fileExists(filename)) |
| 840 | return false; |
| 841 | |
| 842 | if (allowedMIMETypes.isEmpty() && allowedFileExtensions.isEmpty()) |
| 843 | return true; |
| 844 | |
| 845 | // We can't infer a MIME type from a file without an extension, just give up. |
| 846 | auto dotOffset = filename.reverseFind('.'); |
| 847 | if (dotOffset == notFound) |
| 848 | return false; |
| 849 | |
| 850 | String extension = filename.substring(dotOffset + 1).convertToASCIILowercase(); |
| 851 | if (extension.isEmpty()) |
| 852 | return false; |
| 853 | |
| 854 | if (allowedFileExtensions.contains(extension)) |
| 855 | return true; |
| 856 | |
| 857 | String mappedMIMEType = WebCore::MIMETypeRegistry::getMIMETypeForExtension(extension).convertToASCIILowercase(); |
| 858 | if (mappedMIMEType.isEmpty()) |
| 859 | return false; |
| 860 | |
| 861 | if (allowedMIMETypes.contains(mappedMIMEType)) |
| 862 | return true; |
| 863 | |
| 864 | // Fall back to checking for a MIME type wildcard if an exact match is not found. |
| 865 | Vector<String> components = mappedMIMEType.split('/'); |
| 866 | if (components.size() != 2) |
| 867 | return false; |
| 868 | |
| 869 | String wildcardedMIMEType = makeString(components[0], "/*" ); |
| 870 | if (allowedMIMETypes.contains(wildcardedMIMEType)) |
| 871 | return true; |
| 872 | |
| 873 | return false; |
| 874 | } |
| 875 | |
| 876 | void WebAutomationSession::handleRunOpenPanel(const WebPageProxy& page, const WebFrameProxy&, const API::OpenPanelParameters& parameters, WebOpenPanelResultListenerProxy& resultListener) |
| 877 | { |
| 878 | String browsingContextHandle = handleForWebPageProxy(page); |
| 879 | if (!m_filesToSelectForFileUpload.size()) { |
| 880 | resultListener.cancel(); |
| 881 | m_domainNotifier->fileChooserDismissed(browsingContextHandle, true); |
| 882 | return; |
| 883 | } |
| 884 | |
| 885 | if (m_filesToSelectForFileUpload.size() > 1 && !parameters.allowMultipleFiles()) { |
| 886 | resultListener.cancel(); |
| 887 | m_domainNotifier->fileChooserDismissed(browsingContextHandle, true); |
| 888 | return; |
| 889 | } |
| 890 | |
| 891 | HashSet<String> allowedMIMETypes; |
| 892 | auto acceptMIMETypes = parameters.acceptMIMETypes(); |
| 893 | for (auto type : acceptMIMETypes->elementsOfType<API::String>()) |
| 894 | allowedMIMETypes.add(type->string()); |
| 895 | |
| 896 | HashSet<String> allowedFileExtensions; |
| 897 | auto acceptFileExtensions = parameters.acceptFileExtensions(); |
| 898 | for (auto type : acceptFileExtensions->elementsOfType<API::String>()) { |
| 899 | // WebCore vends extensions with leading periods. Strip these to simplify matching later. |
| 900 | String extension = type->string(); |
| 901 | ASSERT(extension.characterAt(0) == '.'); |
| 902 | allowedFileExtensions.add(extension.substring(1)); |
| 903 | } |
| 904 | |
| 905 | // Per §14.3.10.5 in the W3C spec, if at least one file cannot be accepted, the command should fail. |
| 906 | // The REST API service can tell that this failed by checking the "files" attribute of the input element. |
| 907 | for (const String& filename : m_filesToSelectForFileUpload) { |
| 908 | if (!fileCanBeAcceptedForUpload(filename, allowedMIMETypes, allowedFileExtensions)) { |
| 909 | resultListener.cancel(); |
| 910 | m_domainNotifier->fileChooserDismissed(browsingContextHandle, true); |
| 911 | return; |
| 912 | } |
| 913 | } |
| 914 | |
| 915 | resultListener.chooseFiles(m_filesToSelectForFileUpload); |
| 916 | m_domainNotifier->fileChooserDismissed(browsingContextHandle, false); |
| 917 | } |
| 918 | |
| 919 | void WebAutomationSession::evaluateJavaScriptFunction(const String& browsingContextHandle, const String* optionalFrameHandle, const String& function, const JSON::Array& arguments, const bool* optionalExpectsImplicitCallbackArgument, const int* optionalCallbackTimeout, Ref<EvaluateJavaScriptFunctionCallback>&& callback) |
| 920 | { |
| 921 | WebPageProxy* page = webPageProxyForHandle(browsingContextHandle); |
| 922 | if (!page) |
| 923 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
| 924 | |
| 925 | Optional<uint64_t> frameID = webFrameIDForHandle(optionalFrameHandle ? *optionalFrameHandle : emptyString()); |
| 926 | if (!frameID) |
| 927 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(FrameNotFound); |
| 928 | |
| 929 | Vector<String> argumentsVector; |
| 930 | argumentsVector.reserveCapacity(arguments.length()); |
| 931 | |
| 932 | for (auto& argument : arguments) { |
| 933 | String argumentString; |
| 934 | argument->asString(argumentString); |
| 935 | argumentsVector.uncheckedAppend(argumentString); |
| 936 | } |
| 937 | |
| 938 | bool expectsImplicitCallbackArgument = optionalExpectsImplicitCallbackArgument ? *optionalExpectsImplicitCallbackArgument : false; |
| 939 | int callbackTimeout = optionalCallbackTimeout ? *optionalCallbackTimeout : 0; |
| 940 | |
| 941 | uint64_t callbackID = m_nextEvaluateJavaScriptCallbackID++; |
| 942 | m_evaluateJavaScriptFunctionCallbacks.set(callbackID, WTFMove(callback)); |
| 943 | |
| 944 | page->process().send(Messages::WebAutomationSessionProxy::EvaluateJavaScriptFunction(page->pageID(), frameID.value(), function, argumentsVector, expectsImplicitCallbackArgument, callbackTimeout, callbackID), 0); |
| 945 | } |
| 946 | |
| 947 | void WebAutomationSession::didEvaluateJavaScriptFunction(uint64_t callbackID, const String& result, const String& errorType) |
| 948 | { |
| 949 | auto callback = m_evaluateJavaScriptFunctionCallbacks.take(callbackID); |
| 950 | if (!callback) |
| 951 | return; |
| 952 | |
| 953 | if (!errorType.isEmpty()) |
| 954 | callback->sendFailure(STRING_FOR_PREDEFINED_ERROR_MESSAGE_AND_DETAILS(errorType, result)); |
| 955 | else |
| 956 | callback->sendSuccess(result); |
| 957 | } |
| 958 | |
| 959 | void WebAutomationSession::resolveChildFrameHandle(const String& browsingContextHandle, const String* optionalFrameHandle, const int* optionalOrdinal, const String* optionalName, const String* optionalNodeHandle, Ref<ResolveChildFrameHandleCallback>&& callback) |
| 960 | { |
| 961 | if (!optionalOrdinal && !optionalName && !optionalNodeHandle) |
| 962 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(MissingParameter, "Command must specify a child frame by ordinal, name, or element handle." ); |
| 963 | |
| 964 | WebPageProxy* page = webPageProxyForHandle(browsingContextHandle); |
| 965 | if (!page) |
| 966 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
| 967 | |
| 968 | Optional<uint64_t> frameID = webFrameIDForHandle(optionalFrameHandle ? *optionalFrameHandle : emptyString()); |
| 969 | if (!frameID) |
| 970 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(FrameNotFound); |
| 971 | |
| 972 | WTF::CompletionHandler<void(Optional<String>, uint64_t)> completionHandler = [this, protectedThis = makeRef(*this), callback = callback.copyRef()](Optional<String> errorType, uint64_t frameID) mutable { |
| 973 | if (errorType) { |
| 974 | callback->sendFailure(STRING_FOR_PREDEFINED_ERROR_MESSAGE(*errorType)); |
| 975 | return; |
| 976 | } |
| 977 | |
| 978 | callback->sendSuccess(handleForWebFrameID(frameID)); |
| 979 | }; |
| 980 | |
| 981 | if (optionalNodeHandle) { |
| 982 | page->process().sendWithAsyncReply(Messages::WebAutomationSessionProxy::ResolveChildFrameWithNodeHandle(page->pageID(), frameID.value(), *optionalNodeHandle), WTFMove(completionHandler)); |
| 983 | return; |
| 984 | } |
| 985 | |
| 986 | if (optionalName) { |
| 987 | page->process().sendWithAsyncReply(Messages::WebAutomationSessionProxy::ResolveChildFrameWithName(page->pageID(), frameID.value(), *optionalName), WTFMove(completionHandler)); |
| 988 | return; |
| 989 | } |
| 990 | |
| 991 | if (optionalOrdinal) { |
| 992 | page->process().sendWithAsyncReply(Messages::WebAutomationSessionProxy::ResolveChildFrameWithOrdinal(page->pageID(), frameID.value(), *optionalOrdinal), WTFMove(completionHandler)); |
| 993 | return; |
| 994 | } |
| 995 | |
| 996 | ASSERT_NOT_REACHED(); |
| 997 | } |
| 998 | |
| 999 | void WebAutomationSession::resolveParentFrameHandle(const String& browsingContextHandle, const String& frameHandle, Ref<ResolveParentFrameHandleCallback>&& callback) |
| 1000 | { |
| 1001 | WebPageProxy* page = webPageProxyForHandle(browsingContextHandle); |
| 1002 | if (!page) |
| 1003 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
| 1004 | |
| 1005 | Optional<uint64_t> frameID = webFrameIDForHandle(frameHandle); |
| 1006 | if (!frameID) |
| 1007 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(FrameNotFound); |
| 1008 | |
| 1009 | WTF::CompletionHandler<void(Optional<String>, uint64_t)> completionHandler = [this, protectedThis = makeRef(*this), callback = callback.copyRef()](Optional<String> errorType, uint64_t frameID) mutable { |
| 1010 | if (errorType) { |
| 1011 | callback->sendFailure(STRING_FOR_PREDEFINED_ERROR_MESSAGE(*errorType)); |
| 1012 | return; |
| 1013 | } |
| 1014 | |
| 1015 | callback->sendSuccess(handleForWebFrameID(frameID)); |
| 1016 | }; |
| 1017 | |
| 1018 | page->process().sendWithAsyncReply(Messages::WebAutomationSessionProxy::ResolveParentFrame(page->pageID(), frameID.value()), WTFMove(completionHandler)); |
| 1019 | } |
| 1020 | |
| 1021 | static Optional<CoordinateSystem> protocolStringToCoordinateSystem(const String& coordinateSystemString) |
| 1022 | { |
| 1023 | if (coordinateSystemString == "Page" ) |
| 1024 | return CoordinateSystem::Page; |
| 1025 | if (coordinateSystemString == "LayoutViewport" ) |
| 1026 | return CoordinateSystem::LayoutViewport; |
| 1027 | return WTF::nullopt; |
| 1028 | } |
| 1029 | |
| 1030 | void WebAutomationSession::computeElementLayout(const String& browsingContextHandle, const String& frameHandle, const String& nodeHandle, const bool* optionalScrollIntoViewIfNeeded, const String& coordinateSystemString, Ref<ComputeElementLayoutCallback>&& callback) |
| 1031 | { |
| 1032 | WebPageProxy* page = webPageProxyForHandle(browsingContextHandle); |
| 1033 | if (!page) |
| 1034 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
| 1035 | |
| 1036 | Optional<uint64_t> frameID = webFrameIDForHandle(frameHandle); |
| 1037 | if (!frameID) |
| 1038 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(FrameNotFound); |
| 1039 | |
| 1040 | Optional<CoordinateSystem> coordinateSystem = protocolStringToCoordinateSystem(coordinateSystemString); |
| 1041 | if (!coordinateSystem) |
| 1042 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "The parameter 'coordinateSystem' is invalid." ); |
| 1043 | |
| 1044 | WTF::CompletionHandler<void(Optional<String>, WebCore::IntRect, Optional<WebCore::IntPoint>, bool)> completionHandler = [callback = callback.copyRef()](Optional<String> errorType, WebCore::IntRect rect, Optional<WebCore::IntPoint> inViewCenterPoint, bool isObscured) mutable { |
| 1045 | if (errorType) { |
| 1046 | callback->sendFailure(STRING_FOR_PREDEFINED_ERROR_MESSAGE(*errorType)); |
| 1047 | return; |
| 1048 | } |
| 1049 | |
| 1050 | auto originObject = Inspector::Protocol::Automation::Point::create() |
| 1051 | .setX(rect.x()) |
| 1052 | .setY(rect.y()) |
| 1053 | .release(); |
| 1054 | |
| 1055 | auto sizeObject = Inspector::Protocol::Automation::Size::create() |
| 1056 | .setWidth(rect.width()) |
| 1057 | .setHeight(rect.height()) |
| 1058 | .release(); |
| 1059 | |
| 1060 | auto rectObject = Inspector::Protocol::Automation::Rect::create() |
| 1061 | .setOrigin(WTFMove(originObject)) |
| 1062 | .setSize(WTFMove(sizeObject)) |
| 1063 | .release(); |
| 1064 | |
| 1065 | if (!inViewCenterPoint) { |
| 1066 | callback->sendSuccess(WTFMove(rectObject), nullptr, isObscured); |
| 1067 | return; |
| 1068 | } |
| 1069 | |
| 1070 | auto inViewCenterPointObject = Inspector::Protocol::Automation::Point::create() |
| 1071 | .setX(inViewCenterPoint.value().x()) |
| 1072 | .setY(inViewCenterPoint.value().y()) |
| 1073 | .release(); |
| 1074 | |
| 1075 | callback->sendSuccess(WTFMove(rectObject), WTFMove(inViewCenterPointObject), isObscured); |
| 1076 | }; |
| 1077 | |
| 1078 | bool scrollIntoViewIfNeeded = optionalScrollIntoViewIfNeeded ? *optionalScrollIntoViewIfNeeded : false; |
| 1079 | page->process().sendWithAsyncReply(Messages::WebAutomationSessionProxy::ComputeElementLayout(page->pageID(), frameID.value(), nodeHandle, scrollIntoViewIfNeeded, coordinateSystem.value()), WTFMove(completionHandler)); |
| 1080 | } |
| 1081 | |
| 1082 | void WebAutomationSession::selectOptionElement(const String& browsingContextHandle, const String& frameHandle, const String& nodeHandle, Ref<SelectOptionElementCallback>&& callback) |
| 1083 | { |
| 1084 | WebPageProxy* page = webPageProxyForHandle(browsingContextHandle); |
| 1085 | if (!page) |
| 1086 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
| 1087 | |
| 1088 | Optional<uint64_t> frameID = webFrameIDForHandle(frameHandle); |
| 1089 | if (!frameID) |
| 1090 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(FrameNotFound); |
| 1091 | |
| 1092 | WTF::CompletionHandler<void(Optional<String>)> completionHandler = [callback = callback.copyRef()](Optional<String> errorType) mutable { |
| 1093 | if (errorType) { |
| 1094 | callback->sendFailure(STRING_FOR_PREDEFINED_ERROR_MESSAGE(*errorType)); |
| 1095 | return; |
| 1096 | } |
| 1097 | |
| 1098 | callback->sendSuccess(); |
| 1099 | }; |
| 1100 | |
| 1101 | page->process().sendWithAsyncReply(Messages::WebAutomationSessionProxy::SelectOptionElement(page->pageID(), frameID.value(), nodeHandle), WTFMove(completionHandler)); |
| 1102 | } |
| 1103 | |
| 1104 | void WebAutomationSession::isShowingJavaScriptDialog(Inspector::ErrorString& errorString, const String& browsingContextHandle, bool* result) |
| 1105 | { |
| 1106 | ASSERT(m_client); |
| 1107 | if (!m_client) |
| 1108 | SYNC_FAIL_WITH_PREDEFINED_ERROR(InternalError); |
| 1109 | |
| 1110 | WebPageProxy* page = webPageProxyForHandle(browsingContextHandle); |
| 1111 | if (!page) |
| 1112 | SYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
| 1113 | |
| 1114 | *result = m_client->isShowingJavaScriptDialogOnPage(*this, *page); |
| 1115 | } |
| 1116 | |
| 1117 | void WebAutomationSession::dismissCurrentJavaScriptDialog(Inspector::ErrorString& errorString, const String& browsingContextHandle) |
| 1118 | { |
| 1119 | ASSERT(m_client); |
| 1120 | if (!m_client) |
| 1121 | SYNC_FAIL_WITH_PREDEFINED_ERROR(InternalError); |
| 1122 | |
| 1123 | WebPageProxy* page = webPageProxyForHandle(browsingContextHandle); |
| 1124 | if (!page) |
| 1125 | SYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
| 1126 | |
| 1127 | if (!m_client->isShowingJavaScriptDialogOnPage(*this, *page)) |
| 1128 | SYNC_FAIL_WITH_PREDEFINED_ERROR(NoJavaScriptDialog); |
| 1129 | |
| 1130 | m_client->dismissCurrentJavaScriptDialogOnPage(*this, *page); |
| 1131 | } |
| 1132 | |
| 1133 | void WebAutomationSession::acceptCurrentJavaScriptDialog(Inspector::ErrorString& errorString, const String& browsingContextHandle) |
| 1134 | { |
| 1135 | ASSERT(m_client); |
| 1136 | if (!m_client) |
| 1137 | SYNC_FAIL_WITH_PREDEFINED_ERROR(InternalError); |
| 1138 | |
| 1139 | WebPageProxy* page = webPageProxyForHandle(browsingContextHandle); |
| 1140 | if (!page) |
| 1141 | SYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
| 1142 | |
| 1143 | if (!m_client->isShowingJavaScriptDialogOnPage(*this, *page)) |
| 1144 | SYNC_FAIL_WITH_PREDEFINED_ERROR(NoJavaScriptDialog); |
| 1145 | |
| 1146 | m_client->acceptCurrentJavaScriptDialogOnPage(*this, *page); |
| 1147 | } |
| 1148 | |
| 1149 | void WebAutomationSession::messageOfCurrentJavaScriptDialog(Inspector::ErrorString& errorString, const String& browsingContextHandle, String* text) |
| 1150 | { |
| 1151 | ASSERT(m_client); |
| 1152 | if (!m_client) |
| 1153 | SYNC_FAIL_WITH_PREDEFINED_ERROR(InternalError); |
| 1154 | |
| 1155 | WebPageProxy* page = webPageProxyForHandle(browsingContextHandle); |
| 1156 | if (!page) |
| 1157 | SYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
| 1158 | |
| 1159 | if (!m_client->isShowingJavaScriptDialogOnPage(*this, *page)) |
| 1160 | SYNC_FAIL_WITH_PREDEFINED_ERROR(NoJavaScriptDialog); |
| 1161 | |
| 1162 | *text = m_client->messageOfCurrentJavaScriptDialogOnPage(*this, *page); |
| 1163 | } |
| 1164 | |
| 1165 | void WebAutomationSession::setUserInputForCurrentJavaScriptPrompt(Inspector::ErrorString& errorString, const String& browsingContextHandle, const String& promptValue) |
| 1166 | { |
| 1167 | ASSERT(m_client); |
| 1168 | if (!m_client) |
| 1169 | SYNC_FAIL_WITH_PREDEFINED_ERROR(InternalError); |
| 1170 | |
| 1171 | WebPageProxy* page = webPageProxyForHandle(browsingContextHandle); |
| 1172 | if (!page) |
| 1173 | SYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
| 1174 | |
| 1175 | if (!m_client->isShowingJavaScriptDialogOnPage(*this, *page)) |
| 1176 | SYNC_FAIL_WITH_PREDEFINED_ERROR(NoJavaScriptDialog); |
| 1177 | |
| 1178 | // §18.4 Send Alert Text. |
| 1179 | // https://w3c.github.io/webdriver/webdriver-spec.html#send-alert-text |
| 1180 | // 3. Run the substeps of the first matching current user prompt: |
| 1181 | auto scriptDialogType = m_client->typeOfCurrentJavaScriptDialogOnPage(*this, *page); |
| 1182 | ASSERT(scriptDialogType); |
| 1183 | switch (scriptDialogType.value()) { |
| 1184 | case API::AutomationSessionClient::JavaScriptDialogType::Alert: |
| 1185 | case API::AutomationSessionClient::JavaScriptDialogType::Confirm: |
| 1186 | // Return error with error code element not interactable. |
| 1187 | SYNC_FAIL_WITH_PREDEFINED_ERROR(ElementNotInteractable); |
| 1188 | case API::AutomationSessionClient::JavaScriptDialogType::Prompt: |
| 1189 | // Do nothing. |
| 1190 | break; |
| 1191 | case API::AutomationSessionClient::JavaScriptDialogType::BeforeUnloadConfirm: |
| 1192 | // Return error with error code unsupported operation. |
| 1193 | SYNC_FAIL_WITH_PREDEFINED_ERROR(NotImplemented); |
| 1194 | } |
| 1195 | |
| 1196 | m_client->setUserInputForCurrentJavaScriptPromptOnPage(*this, *page, promptValue); |
| 1197 | } |
| 1198 | |
| 1199 | void WebAutomationSession::setFilesToSelectForFileUpload(ErrorString& errorString, const String& browsingContextHandle, const JSON::Array& filenames, const JSON::Array* fileContents) |
| 1200 | { |
| 1201 | Vector<String> newFileList; |
| 1202 | newFileList.reserveInitialCapacity(filenames.length()); |
| 1203 | |
| 1204 | if (fileContents && fileContents->length() != filenames.length()) |
| 1205 | SYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InternalError, "The parameters 'filenames' and 'fileContents' must have equal length." ); |
| 1206 | |
| 1207 | for (size_t i = 0; i < filenames.length(); ++i) { |
| 1208 | String filename; |
| 1209 | if (!filenames.get(i)->asString(filename)) |
| 1210 | SYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InternalError, "The parameter 'filenames' contains a non-string value." ); |
| 1211 | |
| 1212 | if (!fileContents) { |
| 1213 | newFileList.append(filename); |
| 1214 | continue; |
| 1215 | } |
| 1216 | |
| 1217 | String fileData; |
| 1218 | if (!fileContents->get(i)->asString(fileData)) |
| 1219 | SYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InternalError, "The parameter 'fileContents' contains a non-string value." ); |
| 1220 | |
| 1221 | Optional<String> localFilePath = platformGenerateLocalFilePathForRemoteFile(filename, fileData); |
| 1222 | if (!localFilePath) |
| 1223 | SYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InternalError, "The remote file could not be saved to a local temporary directory." ); |
| 1224 | |
| 1225 | newFileList.append(localFilePath.value()); |
| 1226 | } |
| 1227 | |
| 1228 | m_filesToSelectForFileUpload.swap(newFileList); |
| 1229 | } |
| 1230 | |
| 1231 | static Ref<Inspector::Protocol::Automation::Cookie> buildObjectForCookie(const WebCore::Cookie& cookie) |
| 1232 | { |
| 1233 | return Inspector::Protocol::Automation::Cookie::create() |
| 1234 | .setName(cookie.name) |
| 1235 | .setValue(cookie.value) |
| 1236 | .setDomain(cookie.domain) |
| 1237 | .setPath(cookie.path) |
| 1238 | .setExpires(cookie.expires ? cookie.expires / 1000 : 0) |
| 1239 | .setSize((cookie.name.length() + cookie.value.length())) |
| 1240 | .setHttpOnly(cookie.httpOnly) |
| 1241 | .setSecure(cookie.secure) |
| 1242 | .setSession(cookie.session) |
| 1243 | .release(); |
| 1244 | } |
| 1245 | |
| 1246 | static Ref<JSON::ArrayOf<Inspector::Protocol::Automation::Cookie>> buildArrayForCookies(Vector<WebCore::Cookie>& cookiesList) |
| 1247 | { |
| 1248 | auto cookies = JSON::ArrayOf<Inspector::Protocol::Automation::Cookie>::create(); |
| 1249 | |
| 1250 | for (const auto& cookie : cookiesList) |
| 1251 | cookies->addItem(buildObjectForCookie(cookie)); |
| 1252 | |
| 1253 | return cookies; |
| 1254 | } |
| 1255 | |
| 1256 | void WebAutomationSession::getAllCookies(const String& browsingContextHandle, Ref<GetAllCookiesCallback>&& callback) |
| 1257 | { |
| 1258 | WebPageProxy* page = webPageProxyForHandle(browsingContextHandle); |
| 1259 | if (!page) |
| 1260 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
| 1261 | |
| 1262 | WTF::CompletionHandler<void(Optional<String>, Vector<WebCore::Cookie>)> completionHandler = [callback = callback.copyRef()](Optional<String> errorType, Vector<WebCore::Cookie> cookies) mutable { |
| 1263 | if (errorType) { |
| 1264 | callback->sendFailure(STRING_FOR_PREDEFINED_ERROR_MESSAGE(*errorType)); |
| 1265 | return; |
| 1266 | } |
| 1267 | |
| 1268 | callback->sendSuccess(buildArrayForCookies(cookies)); |
| 1269 | }; |
| 1270 | |
| 1271 | // Always send the main frame ID as 0 so it is resolved on the WebProcess side. This avoids a race when page->mainFrame() is null still. |
| 1272 | const uint64_t mainFrameID = 0; |
| 1273 | page->process().sendWithAsyncReply(Messages::WebAutomationSessionProxy::GetCookiesForFrame(page->pageID(), mainFrameID), WTFMove(completionHandler)); |
| 1274 | } |
| 1275 | |
| 1276 | void WebAutomationSession::deleteSingleCookie(const String& browsingContextHandle, const String& cookieName, Ref<DeleteSingleCookieCallback>&& callback) |
| 1277 | { |
| 1278 | WebPageProxy* page = webPageProxyForHandle(browsingContextHandle); |
| 1279 | if (!page) |
| 1280 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
| 1281 | |
| 1282 | WTF::CompletionHandler<void(Optional<String>)> completionHandler = [callback = callback.copyRef()](Optional<String> errorType) mutable { |
| 1283 | if (errorType) { |
| 1284 | callback->sendFailure(STRING_FOR_PREDEFINED_ERROR_MESSAGE(*errorType)); |
| 1285 | return; |
| 1286 | } |
| 1287 | |
| 1288 | callback->sendSuccess(); |
| 1289 | }; |
| 1290 | |
| 1291 | // Always send the main frame ID as 0 so it is resolved on the WebProcess side. This avoids a race when page->mainFrame() is null still. |
| 1292 | const uint64_t mainFrameID = 0; |
| 1293 | page->process().sendWithAsyncReply(Messages::WebAutomationSessionProxy::DeleteCookie(page->pageID(), mainFrameID, cookieName), WTFMove(completionHandler)); |
| 1294 | } |
| 1295 | |
| 1296 | static String domainByAddingDotPrefixIfNeeded(String domain) |
| 1297 | { |
| 1298 | if (domain[0] != '.') { |
| 1299 | // RFC 2965: If an explicitly specified value does not start with a dot, the user agent supplies a leading dot. |
| 1300 | // Assume that any host that ends with a digit is trying to be an IP address. |
| 1301 | if (!URL::hostIsIPAddress(domain)) |
| 1302 | return makeString('.', domain); |
| 1303 | } |
| 1304 | |
| 1305 | return domain; |
| 1306 | } |
| 1307 | |
| 1308 | void WebAutomationSession::addSingleCookie(const String& browsingContextHandle, const JSON::Object& cookieObject, Ref<AddSingleCookieCallback>&& callback) |
| 1309 | { |
| 1310 | WebPageProxy* page = webPageProxyForHandle(browsingContextHandle); |
| 1311 | if (!page) |
| 1312 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
| 1313 | |
| 1314 | URL activeURL = URL(URL(), page->pageLoadState().activeURL()); |
| 1315 | ASSERT(activeURL.isValid()); |
| 1316 | |
| 1317 | WebCore::Cookie cookie; |
| 1318 | |
| 1319 | if (!cookieObject.getString("name"_s , cookie.name)) |
| 1320 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(MissingParameter, "The parameter 'name' was not found." ); |
| 1321 | |
| 1322 | if (!cookieObject.getString("value"_s , cookie.value)) |
| 1323 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(MissingParameter, "The parameter 'value' was not found." ); |
| 1324 | |
| 1325 | String domain; |
| 1326 | if (!cookieObject.getString("domain"_s , domain)) |
| 1327 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(MissingParameter, "The parameter 'domain' was not found." ); |
| 1328 | |
| 1329 | // Inherit the domain/host from the main frame's URL if it is not explicitly set. |
| 1330 | if (domain.isEmpty()) |
| 1331 | cookie.domain = activeURL.host().toString(); |
| 1332 | else |
| 1333 | cookie.domain = domainByAddingDotPrefixIfNeeded(domain); |
| 1334 | |
| 1335 | if (!cookieObject.getString("path"_s , cookie.path)) |
| 1336 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(MissingParameter, "The parameter 'path' was not found." ); |
| 1337 | |
| 1338 | double expires; |
| 1339 | if (!cookieObject.getDouble("expires"_s , expires)) |
| 1340 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(MissingParameter, "The parameter 'expires' was not found." ); |
| 1341 | |
| 1342 | cookie.expires = expires * 1000.0; |
| 1343 | |
| 1344 | if (!cookieObject.getBoolean("secure"_s , cookie.secure)) |
| 1345 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(MissingParameter, "The parameter 'secure' was not found." ); |
| 1346 | |
| 1347 | if (!cookieObject.getBoolean("session"_s , cookie.session)) |
| 1348 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(MissingParameter, "The parameter 'session' was not found." ); |
| 1349 | |
| 1350 | if (!cookieObject.getBoolean("httpOnly"_s , cookie.httpOnly)) |
| 1351 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(MissingParameter, "The parameter 'httpOnly' was not found." ); |
| 1352 | |
| 1353 | WebCookieManagerProxy* cookieManager = m_processPool->supplement<WebCookieManagerProxy>(); |
| 1354 | cookieManager->setCookies(page->websiteDataStore().sessionID(), { cookie }, [callback = callback.copyRef()](CallbackBase::Error error) { |
| 1355 | if (error == CallbackBase::Error::None) |
| 1356 | callback->sendSuccess(); |
| 1357 | else |
| 1358 | callback->sendFailure(STRING_FOR_PREDEFINED_ERROR_NAME(InternalError)); |
| 1359 | }); |
| 1360 | } |
| 1361 | |
| 1362 | void WebAutomationSession::deleteAllCookies(ErrorString& errorString, const String& browsingContextHandle) |
| 1363 | { |
| 1364 | WebPageProxy* page = webPageProxyForHandle(browsingContextHandle); |
| 1365 | if (!page) |
| 1366 | SYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
| 1367 | |
| 1368 | URL activeURL = URL(URL(), page->pageLoadState().activeURL()); |
| 1369 | ASSERT(activeURL.isValid()); |
| 1370 | |
| 1371 | String host = activeURL.host().toString(); |
| 1372 | |
| 1373 | WebCookieManagerProxy* cookieManager = m_processPool->supplement<WebCookieManagerProxy>(); |
| 1374 | cookieManager->deleteCookiesForHostnames(page->websiteDataStore().sessionID(), { host, domainByAddingDotPrefixIfNeeded(host) }); |
| 1375 | } |
| 1376 | |
| 1377 | void WebAutomationSession::getSessionPermissions(ErrorString&, RefPtr<JSON::ArrayOf<Inspector::Protocol::Automation::SessionPermissionData>>& out_permissions) |
| 1378 | { |
| 1379 | auto permissionsObjectArray = JSON::ArrayOf<Inspector::Protocol::Automation::SessionPermissionData>::create(); |
| 1380 | auto getUserMediaPermissionObject = Inspector::Protocol::Automation::SessionPermissionData::create() |
| 1381 | .setPermission(Inspector::Protocol::Automation::SessionPermission::GetUserMedia) |
| 1382 | .setValue(m_permissionForGetUserMedia) |
| 1383 | .release(); |
| 1384 | |
| 1385 | permissionsObjectArray->addItem(WTFMove(getUserMediaPermissionObject)); |
| 1386 | out_permissions = WTFMove(permissionsObjectArray); |
| 1387 | } |
| 1388 | |
| 1389 | void WebAutomationSession::setSessionPermissions(ErrorString& errorString, const JSON::Array& permissions) |
| 1390 | { |
| 1391 | for (auto it = permissions.begin(); it != permissions.end(); ++it) { |
| 1392 | RefPtr<JSON::Object> permission; |
| 1393 | if (!it->get()->asObject(permission)) |
| 1394 | SYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "The parameter 'permissions' is invalid." ); |
| 1395 | |
| 1396 | String permissionName; |
| 1397 | if (!permission->getString("permission"_s , permissionName)) |
| 1398 | SYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "The parameter 'permission' is missing or invalid." ); |
| 1399 | |
| 1400 | auto parsedPermissionName = Inspector::Protocol::AutomationHelpers::parseEnumValueFromString<Inspector::Protocol::Automation::SessionPermission>(permissionName); |
| 1401 | if (!parsedPermissionName) |
| 1402 | SYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "The parameter 'permission' has an unknown value." ); |
| 1403 | |
| 1404 | bool permissionValue; |
| 1405 | if (!permission->getBoolean("value"_s , permissionValue)) |
| 1406 | SYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "The parameter 'value' is missing or invalid." ); |
| 1407 | |
| 1408 | switch (parsedPermissionName.value()) { |
| 1409 | case Inspector::Protocol::Automation::SessionPermission::GetUserMedia: |
| 1410 | m_permissionForGetUserMedia = permissionValue; |
| 1411 | break; |
| 1412 | } |
| 1413 | } |
| 1414 | } |
| 1415 | |
| 1416 | bool WebAutomationSession::shouldAllowGetUserMediaForPage(const WebPageProxy&) const |
| 1417 | { |
| 1418 | return m_permissionForGetUserMedia; |
| 1419 | } |
| 1420 | |
| 1421 | bool WebAutomationSession::isSimulatingUserInteraction() const |
| 1422 | { |
| 1423 | #if ENABLE(WEBDRIVER_MOUSE_INTERACTIONS) |
| 1424 | if (!m_pendingMouseEventsFlushedCallbacksPerPage.isEmpty()) |
| 1425 | return true; |
| 1426 | #endif |
| 1427 | #if ENABLE(WEBDRIVER_KEYBOARD_INTERACTIONS) |
| 1428 | if (!m_pendingKeyboardEventsFlushedCallbacksPerPage.isEmpty()) |
| 1429 | return true; |
| 1430 | #endif |
| 1431 | #if ENABLE(WEBDRIVER_TOUCH_INTERACTIONS) |
| 1432 | if (m_simulatingTouchInteraction) |
| 1433 | return true; |
| 1434 | #endif |
| 1435 | return false; |
| 1436 | } |
| 1437 | |
| 1438 | #if ENABLE(WEBDRIVER_ACTIONS_API) |
| 1439 | SimulatedInputDispatcher& WebAutomationSession::inputDispatcherForPage(WebPageProxy& page) |
| 1440 | { |
| 1441 | return m_inputDispatchersByPage.ensure(page.pageID(), [&] { |
| 1442 | return SimulatedInputDispatcher::create(page, *this); |
| 1443 | }).iterator->value; |
| 1444 | } |
| 1445 | |
| 1446 | SimulatedInputSource* WebAutomationSession::inputSourceForType(SimulatedInputSourceType type) const |
| 1447 | { |
| 1448 | // FIXME: this should use something like Vector's findMatching(). |
| 1449 | for (auto& inputSource : m_inputSources) { |
| 1450 | if (inputSource->type == type) |
| 1451 | return &inputSource.get(); |
| 1452 | } |
| 1453 | |
| 1454 | return nullptr; |
| 1455 | } |
| 1456 | |
| 1457 | // MARK: SimulatedInputDispatcher::Client API |
| 1458 | void WebAutomationSession::viewportInViewCenterPointOfElement(WebPageProxy& page, uint64_t frameID, const String& nodeHandle, Function<void (Optional<WebCore::IntPoint>, Optional<AutomationCommandError>)>&& completionHandler) |
| 1459 | { |
| 1460 | WTF::CompletionHandler<void(Optional<String>, WebCore::IntRect, Optional<WebCore::IntPoint>, bool)> didComputeElementLayoutHandler = [completionHandler = WTFMove(completionHandler)](Optional<String> errorType, WebCore::IntRect, Optional<WebCore::IntPoint> inViewCenterPoint, bool) mutable { |
| 1461 | if (errorType) { |
| 1462 | completionHandler(WTF::nullopt, AUTOMATION_COMMAND_ERROR_WITH_MESSAGE(*errorType)); |
| 1463 | return; |
| 1464 | } |
| 1465 | |
| 1466 | if (!inViewCenterPoint) { |
| 1467 | completionHandler(WTF::nullopt, AUTOMATION_COMMAND_ERROR_WITH_NAME(TargetOutOfBounds)); |
| 1468 | return; |
| 1469 | } |
| 1470 | |
| 1471 | completionHandler(inViewCenterPoint, WTF::nullopt); |
| 1472 | }; |
| 1473 | |
| 1474 | page.process().sendWithAsyncReply(Messages::WebAutomationSessionProxy::ComputeElementLayout(page.pageID(), frameID, nodeHandle, false, CoordinateSystem::LayoutViewport), WTFMove(didComputeElementLayoutHandler)); |
| 1475 | } |
| 1476 | |
| 1477 | #if ENABLE(WEBDRIVER_MOUSE_INTERACTIONS) |
| 1478 | void WebAutomationSession::simulateMouseInteraction(WebPageProxy& page, MouseInteraction interaction, WebMouseEvent::Button mouseButton, const WebCore::IntPoint& locationInViewport, CompletionHandler<void(Optional<AutomationCommandError>)>&& completionHandler) |
| 1479 | { |
| 1480 | page.getWindowFrameWithCallback([this, protectedThis = makeRef(*this), completionHandler = WTFMove(completionHandler), page = makeRef(page), interaction, mouseButton, locationInViewport](WebCore::FloatRect windowFrame) mutable { |
| 1481 | auto clippedX = std::min(std::max(0.0f, (float)locationInViewport.x()), windowFrame.size().width()); |
| 1482 | auto clippedY = std::min(std::max(0.0f, (float)locationInViewport.y()), windowFrame.size().height()); |
| 1483 | if (clippedX != locationInViewport.x() || clippedY != locationInViewport.y()) { |
| 1484 | completionHandler(AUTOMATION_COMMAND_ERROR_WITH_NAME(TargetOutOfBounds)); |
| 1485 | return; |
| 1486 | } |
| 1487 | |
| 1488 | // Bridge the flushed callback to our command's completion handler. |
| 1489 | auto mouseEventsFlushedCallback = [completionHandler = WTFMove(completionHandler)](Optional<AutomationCommandError> error) mutable { |
| 1490 | completionHandler(error); |
| 1491 | }; |
| 1492 | |
| 1493 | auto& callbackInMap = m_pendingMouseEventsFlushedCallbacksPerPage.add(page->pageID(), nullptr).iterator->value; |
| 1494 | if (callbackInMap) |
| 1495 | callbackInMap(AUTOMATION_COMMAND_ERROR_WITH_NAME(Timeout)); |
| 1496 | callbackInMap = WTFMove(mouseEventsFlushedCallback); |
| 1497 | |
| 1498 | platformSimulateMouseInteraction(page, interaction, mouseButton, locationInViewport, OptionSet<WebEvent::Modifier>::fromRaw(m_currentModifiers)); |
| 1499 | |
| 1500 | // If the event does not hit test anything in the window, then it may not have been delivered. |
| 1501 | if (callbackInMap && !page->isProcessingMouseEvents()) { |
| 1502 | auto callbackToCancel = m_pendingMouseEventsFlushedCallbacksPerPage.take(page->pageID()); |
| 1503 | callbackToCancel(WTF::nullopt); |
| 1504 | } |
| 1505 | |
| 1506 | // Otherwise, wait for mouseEventsFlushedCallback to run when all events are handled. |
| 1507 | }); |
| 1508 | } |
| 1509 | #endif // ENABLE(WEBDRIVER_MOUSE_INTERACTIONS) |
| 1510 | |
| 1511 | #if ENABLE(WEBDRIVER_TOUCH_INTERACTIONS) |
| 1512 | void WebAutomationSession::simulateTouchInteraction(WebPageProxy& page, TouchInteraction interaction, const WebCore::IntPoint& locationInViewport, Optional<Seconds> duration, CompletionHandler<void(Optional<AutomationCommandError>)>&& completionHandler) |
| 1513 | { |
| 1514 | #if PLATFORM(IOS_FAMILY) |
| 1515 | WebCore::FloatRect visualViewportBounds = WebCore::FloatRect({ }, page.unobscuredContentRect().size()); |
| 1516 | if (!visualViewportBounds.contains(locationInViewport)) { |
| 1517 | completionHandler(AUTOMATION_COMMAND_ERROR_WITH_NAME(TargetOutOfBounds)); |
| 1518 | return; |
| 1519 | } |
| 1520 | #endif |
| 1521 | |
| 1522 | m_simulatingTouchInteraction = true; |
| 1523 | platformSimulateTouchInteraction(page, interaction, locationInViewport, duration, [this, protectedThis = makeRef(*this), completionHandler = WTFMove(completionHandler)](Optional<AutomationCommandError> error) mutable { |
| 1524 | m_simulatingTouchInteraction = false; |
| 1525 | completionHandler(error); |
| 1526 | }); |
| 1527 | } |
| 1528 | #endif // ENABLE(WEBDRIVER_TOUCH_INTERACTIONS) |
| 1529 | |
| 1530 | #if ENABLE(WEBDRIVER_KEYBOARD_INTERACTIONS) |
| 1531 | void WebAutomationSession::simulateKeyboardInteraction(WebPageProxy& page, KeyboardInteraction interaction, WTF::Variant<VirtualKey, CharKey>&& key, CompletionHandler<void(Optional<AutomationCommandError>)>&& completionHandler) |
| 1532 | { |
| 1533 | // Bridge the flushed callback to our command's completion handler. |
| 1534 | auto keyboardEventsFlushedCallback = [completionHandler = WTFMove(completionHandler)](Optional<AutomationCommandError> error) mutable { |
| 1535 | completionHandler(error); |
| 1536 | }; |
| 1537 | |
| 1538 | auto& callbackInMap = m_pendingKeyboardEventsFlushedCallbacksPerPage.add(page.pageID(), nullptr).iterator->value; |
| 1539 | if (callbackInMap) |
| 1540 | callbackInMap(AUTOMATION_COMMAND_ERROR_WITH_NAME(Timeout)); |
| 1541 | callbackInMap = WTFMove(keyboardEventsFlushedCallback); |
| 1542 | |
| 1543 | platformSimulateKeyboardInteraction(page, interaction, WTFMove(key)); |
| 1544 | |
| 1545 | // If the interaction does not generate any events, then do not wait for events to be flushed. |
| 1546 | // This happens in some corner cases on macOS, such as releasing a key while Command is pressed. |
| 1547 | if (callbackInMap && !page.isProcessingKeyboardEvents()) { |
| 1548 | auto callbackToCancel = m_pendingKeyboardEventsFlushedCallbacksPerPage.take(page.pageID()); |
| 1549 | callbackToCancel(WTF::nullopt); |
| 1550 | } |
| 1551 | |
| 1552 | // Otherwise, wait for keyboardEventsFlushedCallback to run when all events are handled. |
| 1553 | } |
| 1554 | #endif // ENABLE(WEBDRIVER_KEYBOARD_INTERACTIONS) |
| 1555 | #endif // ENABLE(WEBDRIVER_ACTIONS_API) |
| 1556 | |
| 1557 | #if ENABLE(WEBDRIVER_MOUSE_INTERACTIONS) |
| 1558 | static WebEvent::Modifier protocolModifierToWebEventModifier(Inspector::Protocol::Automation::KeyModifier modifier) |
| 1559 | { |
| 1560 | switch (modifier) { |
| 1561 | case Inspector::Protocol::Automation::KeyModifier::Alt: |
| 1562 | return WebEvent::Modifier::AltKey; |
| 1563 | case Inspector::Protocol::Automation::KeyModifier::Meta: |
| 1564 | return WebEvent::Modifier::MetaKey; |
| 1565 | case Inspector::Protocol::Automation::KeyModifier::Control: |
| 1566 | return WebEvent::Modifier::ControlKey; |
| 1567 | case Inspector::Protocol::Automation::KeyModifier::Shift: |
| 1568 | return WebEvent::Modifier::ShiftKey; |
| 1569 | case Inspector::Protocol::Automation::KeyModifier::CapsLock: |
| 1570 | return WebEvent::Modifier::CapsLockKey; |
| 1571 | } |
| 1572 | |
| 1573 | RELEASE_ASSERT_NOT_REACHED(); |
| 1574 | } |
| 1575 | #endif // ENABLE(WEBDRIVER_MOUSE_INTERACTIONS) |
| 1576 | |
| 1577 | #if ENABLE(WEBDRIVER_ACTIONS_API) |
| 1578 | static WebMouseEvent::Button protocolMouseButtonToWebMouseEventButton(Inspector::Protocol::Automation::MouseButton button) |
| 1579 | { |
| 1580 | switch (button) { |
| 1581 | case Inspector::Protocol::Automation::MouseButton::None: |
| 1582 | return WebMouseEvent::NoButton; |
| 1583 | case Inspector::Protocol::Automation::MouseButton::Left: |
| 1584 | return WebMouseEvent::LeftButton; |
| 1585 | case Inspector::Protocol::Automation::MouseButton::Middle: |
| 1586 | return WebMouseEvent::MiddleButton; |
| 1587 | case Inspector::Protocol::Automation::MouseButton::Right: |
| 1588 | return WebMouseEvent::RightButton; |
| 1589 | } |
| 1590 | |
| 1591 | RELEASE_ASSERT_NOT_REACHED(); |
| 1592 | } |
| 1593 | #endif // ENABLE(WEBDRIVER_ACTIONS_API) |
| 1594 | |
| 1595 | void WebAutomationSession::performMouseInteraction(const String& handle, const JSON::Object& requestedPositionObject, const String& mouseButtonString, const String& mouseInteractionString, const JSON::Array& keyModifierStrings, Ref<PerformMouseInteractionCallback>&& callback) |
| 1596 | { |
| 1597 | #if !ENABLE(WEBDRIVER_MOUSE_INTERACTIONS) |
| 1598 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(NotImplemented); |
| 1599 | #else |
| 1600 | WebPageProxy* page = webPageProxyForHandle(handle); |
| 1601 | if (!page) |
| 1602 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
| 1603 | |
| 1604 | float x; |
| 1605 | if (!requestedPositionObject.getDouble("x"_s , x)) |
| 1606 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(MissingParameter, "The parameter 'x' was not found." ); |
| 1607 | |
| 1608 | float y; |
| 1609 | if (!requestedPositionObject.getDouble("y"_s , y)) |
| 1610 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(MissingParameter, "The parameter 'y' was not found." ); |
| 1611 | |
| 1612 | OptionSet<WebEvent::Modifier> keyModifiers; |
| 1613 | for (auto it = keyModifierStrings.begin(); it != keyModifierStrings.end(); ++it) { |
| 1614 | String modifierString; |
| 1615 | if (!it->get()->asString(modifierString)) |
| 1616 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "The parameter 'modifiers' is invalid." ); |
| 1617 | |
| 1618 | auto parsedModifier = Inspector::Protocol::AutomationHelpers::parseEnumValueFromString<Inspector::Protocol::Automation::KeyModifier>(modifierString); |
| 1619 | if (!parsedModifier) |
| 1620 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "A modifier in the 'modifiers' array is invalid." ); |
| 1621 | keyModifiers.add(protocolModifierToWebEventModifier(parsedModifier.value())); |
| 1622 | } |
| 1623 | |
| 1624 | page->getWindowFrameWithCallback([this, protectedThis = makeRef(*this), callback = WTFMove(callback), page = makeRef(*page), x, y, mouseInteractionString, mouseButtonString, keyModifiers](WebCore::FloatRect windowFrame) mutable { |
| 1625 | |
| 1626 | x = std::min(std::max(0.0f, x), windowFrame.size().width()); |
| 1627 | y = std::min(std::max(0.0f, y + page->topContentInset()), windowFrame.size().height()); |
| 1628 | |
| 1629 | WebCore::IntPoint positionInView = WebCore::IntPoint(static_cast<int>(x), static_cast<int>(y)); |
| 1630 | |
| 1631 | auto parsedInteraction = Inspector::Protocol::AutomationHelpers::parseEnumValueFromString<Inspector::Protocol::Automation::MouseInteraction>(mouseInteractionString); |
| 1632 | if (!parsedInteraction) |
| 1633 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "The parameter 'interaction' is invalid." ); |
| 1634 | |
| 1635 | auto parsedButton = Inspector::Protocol::AutomationHelpers::parseEnumValueFromString<Inspector::Protocol::Automation::MouseButton>(mouseButtonString); |
| 1636 | if (!parsedButton) |
| 1637 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "The parameter 'button' is invalid." ); |
| 1638 | |
| 1639 | auto mouseEventsFlushedCallback = [protectedThis = WTFMove(protectedThis), callback = WTFMove(callback), page = page.copyRef(), x, y](Optional<AutomationCommandError> error) { |
| 1640 | if (error) |
| 1641 | callback->sendFailure(error.value().toProtocolString()); |
| 1642 | else { |
| 1643 | callback->sendSuccess(Inspector::Protocol::Automation::Point::create() |
| 1644 | .setX(x) |
| 1645 | .setY(y - page->topContentInset()) |
| 1646 | .release()); |
| 1647 | } |
| 1648 | }; |
| 1649 | |
| 1650 | auto& callbackInMap = m_pendingMouseEventsFlushedCallbacksPerPage.add(page->pageID(), nullptr).iterator->value; |
| 1651 | if (callbackInMap) |
| 1652 | callbackInMap(AUTOMATION_COMMAND_ERROR_WITH_NAME(Timeout)); |
| 1653 | callbackInMap = WTFMove(mouseEventsFlushedCallback); |
| 1654 | |
| 1655 | platformSimulateMouseInteraction(page, parsedInteraction.value(), protocolMouseButtonToWebMouseEventButton(parsedButton.value()), positionInView, keyModifiers); |
| 1656 | |
| 1657 | // If the event location was previously clipped and does not hit test anything in the window, then it will not be processed. |
| 1658 | // For compatibility with pre-W3C driver implementations, don't make this a hard error; just do nothing silently. |
| 1659 | // In W3C-only code paths, we can reject any pointer actions whose coordinates are outside the viewport rect. |
| 1660 | if (callbackInMap && !page->isProcessingMouseEvents()) { |
| 1661 | auto callbackToCancel = m_pendingMouseEventsFlushedCallbacksPerPage.take(page->pageID()); |
| 1662 | callbackToCancel(WTF::nullopt); |
| 1663 | } |
| 1664 | }); |
| 1665 | #endif // ENABLE(WEBDRIVER_MOUSE_INTERACTIONS) |
| 1666 | } |
| 1667 | |
| 1668 | void WebAutomationSession::performKeyboardInteractions(const String& handle, const JSON::Array& interactions, Ref<PerformKeyboardInteractionsCallback>&& callback) |
| 1669 | { |
| 1670 | #if !ENABLE(WEBDRIVER_KEYBOARD_INTERACTIONS) |
| 1671 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(NotImplemented); |
| 1672 | #else |
| 1673 | WebPageProxy* page = webPageProxyForHandle(handle); |
| 1674 | if (!page) |
| 1675 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
| 1676 | |
| 1677 | if (!interactions.length()) |
| 1678 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "The parameter 'interactions' was not found or empty." ); |
| 1679 | |
| 1680 | // Validate all of the parameters before performing any interactions with the browsing context under test. |
| 1681 | Vector<WTF::Function<void()>> actionsToPerform; |
| 1682 | actionsToPerform.reserveCapacity(interactions.length()); |
| 1683 | |
| 1684 | for (const auto& interaction : interactions) { |
| 1685 | RefPtr<JSON::Object> interactionObject; |
| 1686 | if (!interaction->asObject(interactionObject)) |
| 1687 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "An interaction in the 'interactions' parameter was invalid." ); |
| 1688 | |
| 1689 | String interactionTypeString; |
| 1690 | if (!interactionObject->getString("type"_s , interactionTypeString)) |
| 1691 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "An interaction in the 'interactions' parameter is missing the 'type' key." ); |
| 1692 | auto interactionType = Inspector::Protocol::AutomationHelpers::parseEnumValueFromString<Inspector::Protocol::Automation::KeyboardInteractionType>(interactionTypeString); |
| 1693 | if (!interactionType) |
| 1694 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "An interaction in the 'interactions' parameter has an invalid 'type' key." ); |
| 1695 | |
| 1696 | String virtualKeyString; |
| 1697 | bool foundVirtualKey = interactionObject->getString("key"_s , virtualKeyString); |
| 1698 | if (foundVirtualKey) { |
| 1699 | Optional<VirtualKey> virtualKey = Inspector::Protocol::AutomationHelpers::parseEnumValueFromString<Inspector::Protocol::Automation::VirtualKey>(virtualKeyString); |
| 1700 | if (!virtualKey) |
| 1701 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "An interaction in the 'interactions' parameter has an invalid 'key' value." ); |
| 1702 | |
| 1703 | actionsToPerform.uncheckedAppend([this, page, interactionType, virtualKey] { |
| 1704 | platformSimulateKeyboardInteraction(*page, interactionType.value(), virtualKey.value()); |
| 1705 | }); |
| 1706 | } |
| 1707 | |
| 1708 | String keySequence; |
| 1709 | bool foundKeySequence = interactionObject->getString("text"_s , keySequence); |
| 1710 | if (foundKeySequence) { |
| 1711 | switch (interactionType.value()) { |
| 1712 | case Inspector::Protocol::Automation::KeyboardInteractionType::KeyPress: |
| 1713 | case Inspector::Protocol::Automation::KeyboardInteractionType::KeyRelease: |
| 1714 | // 'KeyPress' and 'KeyRelease' are meant for a virtual key and are not supported for a string (sequence of codepoints). |
| 1715 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "An interaction in the 'interactions' parameter has an invalid 'key' value." ); |
| 1716 | |
| 1717 | case Inspector::Protocol::Automation::KeyboardInteractionType::InsertByKey: |
| 1718 | actionsToPerform.uncheckedAppend([this, page, keySequence] { |
| 1719 | platformSimulateKeySequence(*page, keySequence); |
| 1720 | }); |
| 1721 | break; |
| 1722 | } |
| 1723 | } |
| 1724 | |
| 1725 | if (!foundVirtualKey && !foundKeySequence) |
| 1726 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(MissingParameter, "An interaction in the 'interactions' parameter is missing both 'key' and 'text'. One must be provided." ); |
| 1727 | } |
| 1728 | |
| 1729 | ASSERT(actionsToPerform.size()); |
| 1730 | if (!actionsToPerform.size()) |
| 1731 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InternalError, "No actions to perform." ); |
| 1732 | |
| 1733 | auto keyboardEventsFlushedCallback = [protectedThis = makeRef(*this), callback = WTFMove(callback), page = makeRef(*page)](Optional<AutomationCommandError> error) { |
| 1734 | if (error) |
| 1735 | callback->sendFailure(error.value().toProtocolString()); |
| 1736 | else |
| 1737 | callback->sendSuccess(); |
| 1738 | }; |
| 1739 | |
| 1740 | auto& callbackInMap = m_pendingKeyboardEventsFlushedCallbacksPerPage.add(page->pageID(), nullptr).iterator->value; |
| 1741 | if (callbackInMap) |
| 1742 | callbackInMap(AUTOMATION_COMMAND_ERROR_WITH_NAME(Timeout)); |
| 1743 | callbackInMap = WTFMove(keyboardEventsFlushedCallback); |
| 1744 | |
| 1745 | for (auto& action : actionsToPerform) |
| 1746 | action(); |
| 1747 | #endif // ENABLE(WEBDRIVER_KEYBOARD_INTERACTIONS) |
| 1748 | } |
| 1749 | |
| 1750 | #if ENABLE(WEBDRIVER_ACTIONS_API) |
| 1751 | static SimulatedInputSourceType simulatedInputSourceTypeFromProtocolSourceType(Inspector::Protocol::Automation::InputSourceType protocolType) |
| 1752 | { |
| 1753 | switch (protocolType) { |
| 1754 | case Inspector::Protocol::Automation::InputSourceType::Null: |
| 1755 | return SimulatedInputSourceType::Null; |
| 1756 | case Inspector::Protocol::Automation::InputSourceType::Keyboard: |
| 1757 | return SimulatedInputSourceType::Keyboard; |
| 1758 | case Inspector::Protocol::Automation::InputSourceType::Mouse: |
| 1759 | return SimulatedInputSourceType::Mouse; |
| 1760 | case Inspector::Protocol::Automation::InputSourceType::Touch: |
| 1761 | return SimulatedInputSourceType::Touch; |
| 1762 | } |
| 1763 | |
| 1764 | RELEASE_ASSERT_NOT_REACHED(); |
| 1765 | } |
| 1766 | #endif // ENABLE(WEBDRIVER_ACTIONS_API) |
| 1767 | |
| 1768 | void WebAutomationSession::performInteractionSequence(const String& handle, const String* optionalFrameHandle, const JSON::Array& inputSources, const JSON::Array& steps, Ref<WebAutomationSession::PerformInteractionSequenceCallback>&& callback) |
| 1769 | { |
| 1770 | // This command implements WebKit support for §17.5 Perform Actions. |
| 1771 | |
| 1772 | #if !ENABLE(WEBDRIVER_ACTIONS_API) |
| 1773 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(NotImplemented); |
| 1774 | #else |
| 1775 | WebPageProxy* page = webPageProxyForHandle(handle); |
| 1776 | if (!page) |
| 1777 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
| 1778 | |
| 1779 | auto frameID = webFrameIDForHandle(optionalFrameHandle ? *optionalFrameHandle : emptyString()); |
| 1780 | if (!frameID) |
| 1781 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(FrameNotFound); |
| 1782 | |
| 1783 | HashMap<String, Ref<SimulatedInputSource>> sourceIdToInputSourceMap; |
| 1784 | HashMap<SimulatedInputSourceType, String, WTF::IntHash<SimulatedInputSourceType>, WTF::StrongEnumHashTraits<SimulatedInputSourceType>> typeToSourceIdMap; |
| 1785 | |
| 1786 | // Parse and validate Automation protocol arguments. By this point, the driver has |
| 1787 | // already performed the steps in §17.3 Processing Actions Requests. |
| 1788 | if (!inputSources.length()) |
| 1789 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "The parameter 'inputSources' was not found or empty." ); |
| 1790 | |
| 1791 | for (const auto& inputSource : inputSources) { |
| 1792 | RefPtr<JSON::Object> inputSourceObject; |
| 1793 | if (!inputSource->asObject(inputSourceObject)) |
| 1794 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "An input source in the 'inputSources' parameter was invalid." ); |
| 1795 | |
| 1796 | String sourceId; |
| 1797 | if (!inputSourceObject->getString("sourceId"_s , sourceId)) |
| 1798 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "An input source in the 'inputSources' parameter is missing a 'sourceId'." ); |
| 1799 | |
| 1800 | String sourceType; |
| 1801 | if (!inputSourceObject->getString("sourceType"_s , sourceType)) |
| 1802 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "An input source in the 'inputSources' parameter is missing a 'sourceType'." ); |
| 1803 | |
| 1804 | auto parsedInputSourceType = Inspector::Protocol::AutomationHelpers::parseEnumValueFromString<Inspector::Protocol::Automation::InputSourceType>(sourceType); |
| 1805 | if (!parsedInputSourceType) |
| 1806 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "An input source in the 'inputSources' parameter has an invalid 'sourceType'." ); |
| 1807 | |
| 1808 | SimulatedInputSourceType inputSourceType = simulatedInputSourceTypeFromProtocolSourceType(*parsedInputSourceType); |
| 1809 | |
| 1810 | // Note: iOS does not support mouse input sources, and other platforms do not support touch input sources. |
| 1811 | // If a mismatch happens, alias to the supported input source. This works because both Mouse and Touch input sources |
| 1812 | // use a MouseButton to indicate the result of interacting (down/up/move), which can be interpreted for touch or mouse. |
| 1813 | #if !ENABLE(WEBDRIVER_MOUSE_INTERACTIONS) && ENABLE(WEBDRIVER_TOUCH_INTERACTIONS) |
| 1814 | if (inputSourceType == SimulatedInputSourceType::Mouse) |
| 1815 | inputSourceType = SimulatedInputSourceType::Touch; |
| 1816 | #endif |
| 1817 | #if !ENABLE(WEBDRIVER_MOUSE_INTERACTIONS) |
| 1818 | if (inputSourceType == SimulatedInputSourceType::Mouse) |
| 1819 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(NotImplemented, "Mouse input sources are not yet supported." ); |
| 1820 | #endif |
| 1821 | #if !ENABLE(WEBDRIVER_TOUCH_INTERACTIONS) |
| 1822 | if (inputSourceType == SimulatedInputSourceType::Touch) |
| 1823 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(NotImplemented, "Touch input sources are not yet supported." ); |
| 1824 | #endif |
| 1825 | #if !ENABLE(WEBDRIVER_KEYBOARD_INTERACTIONS) |
| 1826 | if (inputSourceType == SimulatedInputSourceType::Keyboard) |
| 1827 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(NotImplemented, "Keyboard input sources are not yet supported." ); |
| 1828 | #endif |
| 1829 | if (typeToSourceIdMap.contains(inputSourceType)) |
| 1830 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "Two input sources with the same type were specified." ); |
| 1831 | if (sourceIdToInputSourceMap.contains(sourceId)) |
| 1832 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "Two input sources with the same sourceId were specified." ); |
| 1833 | |
| 1834 | typeToSourceIdMap.add(inputSourceType, sourceId); |
| 1835 | sourceIdToInputSourceMap.add(sourceId, *inputSourceForType(inputSourceType)); |
| 1836 | } |
| 1837 | |
| 1838 | Vector<SimulatedInputKeyFrame> keyFrames; |
| 1839 | |
| 1840 | if (!steps.length()) |
| 1841 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "The parameter 'steps' was not found or empty." ); |
| 1842 | |
| 1843 | for (const auto& step : steps) { |
| 1844 | RefPtr<JSON::Object> stepObject; |
| 1845 | if (!step->asObject(stepObject)) |
| 1846 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "A step in the 'steps' parameter was not an object." ); |
| 1847 | |
| 1848 | RefPtr<JSON::Array> stepStates; |
| 1849 | if (!stepObject->getArray("states"_s , stepStates)) |
| 1850 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "A step is missing the 'states' property." ); |
| 1851 | |
| 1852 | Vector<SimulatedInputKeyFrame::StateEntry> entries; |
| 1853 | entries.reserveCapacity(stepStates->length()); |
| 1854 | |
| 1855 | for (const auto& state : *stepStates) { |
| 1856 | RefPtr<JSON::Object> stateObject; |
| 1857 | if (!state->asObject(stateObject)) |
| 1858 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "Encountered a non-object step state." ); |
| 1859 | |
| 1860 | String sourceId; |
| 1861 | if (!stateObject->getString("sourceId"_s , sourceId)) |
| 1862 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "Step state lacks required 'sourceId' property." ); |
| 1863 | |
| 1864 | if (!sourceIdToInputSourceMap.contains(sourceId)) |
| 1865 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "Unknown 'sourceId' specified." ); |
| 1866 | |
| 1867 | SimulatedInputSource& inputSource = *sourceIdToInputSourceMap.get(sourceId); |
| 1868 | SimulatedInputSourceState sourceState { }; |
| 1869 | |
| 1870 | String pressedCharKeyString; |
| 1871 | if (stateObject->getString("pressedCharKey"_s , pressedCharKeyString)) |
| 1872 | sourceState.pressedCharKey = pressedCharKeyString.characterAt(0); |
| 1873 | |
| 1874 | RefPtr<JSON::Array> pressedVirtualKeysArray; |
| 1875 | if (stateObject->getArray("pressedVirtualKeys"_s , pressedVirtualKeysArray)) { |
| 1876 | VirtualKeySet pressedVirtualKeys { }; |
| 1877 | |
| 1878 | for (auto it = pressedVirtualKeysArray->begin(); it != pressedVirtualKeysArray->end(); ++it) { |
| 1879 | String pressedVirtualKeyString; |
| 1880 | if (!(*it)->asString(pressedVirtualKeyString)) |
| 1881 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "Encountered a non-string virtual key value." ); |
| 1882 | |
| 1883 | Optional<VirtualKey> parsedVirtualKey = Inspector::Protocol::AutomationHelpers::parseEnumValueFromString<Inspector::Protocol::Automation::VirtualKey>(pressedVirtualKeyString); |
| 1884 | if (!parsedVirtualKey) |
| 1885 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "Encountered an unknown virtual key value." ); |
| 1886 | else |
| 1887 | pressedVirtualKeys.add(parsedVirtualKey.value()); |
| 1888 | } |
| 1889 | |
| 1890 | sourceState.pressedVirtualKeys = pressedVirtualKeys; |
| 1891 | } |
| 1892 | |
| 1893 | String pressedButtonString; |
| 1894 | if (stateObject->getString("pressedButton"_s , pressedButtonString)) { |
| 1895 | auto protocolButton = Inspector::Protocol::AutomationHelpers::parseEnumValueFromString<Inspector::Protocol::Automation::MouseButton>(pressedButtonString); |
| 1896 | sourceState.pressedMouseButton = protocolMouseButtonToWebMouseEventButton(protocolButton.valueOr(Inspector::Protocol::Automation::MouseButton::None)); |
| 1897 | } |
| 1898 | |
| 1899 | String originString; |
| 1900 | if (stateObject->getString("origin"_s , originString)) |
| 1901 | sourceState.origin = Inspector::Protocol::AutomationHelpers::parseEnumValueFromString<Inspector::Protocol::Automation::MouseMoveOrigin>(originString); |
| 1902 | |
| 1903 | if (sourceState.origin && sourceState.origin.value() == Inspector::Protocol::Automation::MouseMoveOrigin::Element) { |
| 1904 | String nodeHandleString; |
| 1905 | if (!stateObject->getString("nodeHandle"_s , nodeHandleString)) |
| 1906 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "Node handle not provided for 'Element' origin" ); |
| 1907 | sourceState.nodeHandle = nodeHandleString; |
| 1908 | } |
| 1909 | |
| 1910 | RefPtr<JSON::Object> locationObject; |
| 1911 | if (stateObject->getObject("location"_s , locationObject)) { |
| 1912 | int x, y; |
| 1913 | if (locationObject->getInteger("x"_s , x) && locationObject->getInteger("y"_s , y)) |
| 1914 | sourceState.location = WebCore::IntPoint(x, y); |
| 1915 | } |
| 1916 | |
| 1917 | int parsedDuration; |
| 1918 | if (stateObject->getInteger("duration"_s , parsedDuration)) |
| 1919 | sourceState.duration = Seconds::fromMilliseconds(parsedDuration); |
| 1920 | |
| 1921 | entries.uncheckedAppend(std::pair<SimulatedInputSource&, SimulatedInputSourceState> { inputSource, sourceState }); |
| 1922 | } |
| 1923 | |
| 1924 | keyFrames.append(SimulatedInputKeyFrame(WTFMove(entries))); |
| 1925 | } |
| 1926 | |
| 1927 | SimulatedInputDispatcher& inputDispatcher = inputDispatcherForPage(*page); |
| 1928 | if (inputDispatcher.isActive()) { |
| 1929 | ASSERT_NOT_REACHED(); |
| 1930 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InternalError, "A previous interaction is still underway." ); |
| 1931 | } |
| 1932 | |
| 1933 | // Delegate the rest of §17.4 Dispatching Actions to the dispatcher. |
| 1934 | inputDispatcher.run(frameID.value(), WTFMove(keyFrames), m_inputSources, [protectedThis = makeRef(*this), callback = WTFMove(callback)](Optional<AutomationCommandError> error) { |
| 1935 | if (error) |
| 1936 | callback->sendFailure(error.value().toProtocolString()); |
| 1937 | else |
| 1938 | callback->sendSuccess(); |
| 1939 | }); |
| 1940 | #endif // ENABLE(WEBDRIVER_ACTIONS_API) |
| 1941 | } |
| 1942 | |
| 1943 | void WebAutomationSession::cancelInteractionSequence(const String& handle, const String* optionalFrameHandle, Ref<CancelInteractionSequenceCallback>&& callback) |
| 1944 | { |
| 1945 | // This command implements WebKit support for §17.6 Release Actions. |
| 1946 | |
| 1947 | #if !ENABLE(WEBDRIVER_ACTIONS_API) |
| 1948 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(NotImplemented); |
| 1949 | #else |
| 1950 | WebPageProxy* page = webPageProxyForHandle(handle); |
| 1951 | if (!page) |
| 1952 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
| 1953 | |
| 1954 | auto frameID = webFrameIDForHandle(optionalFrameHandle ? *optionalFrameHandle : emptyString()); |
| 1955 | if (!frameID) |
| 1956 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(FrameNotFound); |
| 1957 | |
| 1958 | Vector<SimulatedInputKeyFrame> keyFrames({ SimulatedInputKeyFrame::keyFrameToResetInputSources(m_inputSources) }); |
| 1959 | SimulatedInputDispatcher& inputDispatcher = inputDispatcherForPage(*page); |
| 1960 | inputDispatcher.cancel(); |
| 1961 | |
| 1962 | inputDispatcher.run(frameID.value(), WTFMove(keyFrames), m_inputSources, [protectedThis = makeRef(*this), callback = WTFMove(callback)](Optional<AutomationCommandError> error) { |
| 1963 | if (error) |
| 1964 | callback->sendFailure(error.value().toProtocolString()); |
| 1965 | else |
| 1966 | callback->sendSuccess(); |
| 1967 | }); |
| 1968 | #endif // ENABLE(WEBDRIVER_ACTIONS_API) |
| 1969 | } |
| 1970 | |
| 1971 | void WebAutomationSession::takeScreenshot(const String& handle, const String* optionalFrameHandle, const String* optionalNodeHandle, const bool* optionalScrollIntoViewIfNeeded, const bool* optionalClipToViewport, Ref<TakeScreenshotCallback>&& callback) |
| 1972 | { |
| 1973 | WebPageProxy* page = webPageProxyForHandle(handle); |
| 1974 | if (!page) |
| 1975 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
| 1976 | |
| 1977 | Optional<uint64_t> frameID = webFrameIDForHandle(optionalFrameHandle ? *optionalFrameHandle : emptyString()); |
| 1978 | if (!frameID) |
| 1979 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(FrameNotFound); |
| 1980 | |
| 1981 | bool scrollIntoViewIfNeeded = optionalScrollIntoViewIfNeeded ? *optionalScrollIntoViewIfNeeded : false; |
| 1982 | String nodeHandle = optionalNodeHandle ? *optionalNodeHandle : emptyString(); |
| 1983 | bool clipToViewport = optionalClipToViewport ? *optionalClipToViewport : false; |
| 1984 | |
| 1985 | uint64_t callbackID = m_nextScreenshotCallbackID++; |
| 1986 | m_screenshotCallbacks.set(callbackID, WTFMove(callback)); |
| 1987 | |
| 1988 | page->process().send(Messages::WebAutomationSessionProxy::TakeScreenshot(page->pageID(), frameID.value(), nodeHandle, scrollIntoViewIfNeeded, clipToViewport, callbackID), 0); |
| 1989 | } |
| 1990 | |
| 1991 | void WebAutomationSession::didTakeScreenshot(uint64_t callbackID, const ShareableBitmap::Handle& imageDataHandle, const String& errorType) |
| 1992 | { |
| 1993 | auto callback = m_screenshotCallbacks.take(callbackID); |
| 1994 | if (!callback) |
| 1995 | return; |
| 1996 | |
| 1997 | if (!errorType.isEmpty()) { |
| 1998 | callback->sendFailure(STRING_FOR_PREDEFINED_ERROR_MESSAGE(errorType)); |
| 1999 | return; |
| 2000 | } |
| 2001 | |
| 2002 | Optional<String> base64EncodedData = platformGetBase64EncodedPNGData(imageDataHandle); |
| 2003 | if (!base64EncodedData) |
| 2004 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(InternalError); |
| 2005 | |
| 2006 | callback->sendSuccess(base64EncodedData.value()); |
| 2007 | } |
| 2008 | |
| 2009 | #if !PLATFORM(COCOA) && !USE(CAIRO) |
| 2010 | Optional<String> WebAutomationSession::platformGetBase64EncodedPNGData(const ShareableBitmap::Handle&) |
| 2011 | { |
| 2012 | return WTF::nullopt; |
| 2013 | } |
| 2014 | #endif // !PLATFORM(COCOA) && !USE(CAIRO) |
| 2015 | |
| 2016 | #if !PLATFORM(COCOA) |
| 2017 | Optional<String> WebAutomationSession::platformGenerateLocalFilePathForRemoteFile(const String&, const String&) |
| 2018 | { |
| 2019 | return WTF::nullopt; |
| 2020 | } |
| 2021 | #endif // !PLATFORM(COCOA) |
| 2022 | |
| 2023 | } // namespace WebKit |
| 2024 | |