diff --git a/Userland/Libraries/LibWeb/CMakeLists.txt b/Userland/Libraries/LibWeb/CMakeLists.txt index c611511db59..b022eb040a4 100644 --- a/Userland/Libraries/LibWeb/CMakeLists.txt +++ b/Userland/Libraries/LibWeb/CMakeLists.txt @@ -761,6 +761,7 @@ set(SOURCES WebDriver/HeapTimer.cpp WebDriver/InputSource.cpp WebDriver/InputState.cpp + WebDriver/JSON.cpp WebDriver/Response.cpp WebDriver/Screenshot.cpp WebDriver/TimeoutsConfiguration.cpp diff --git a/Userland/Libraries/LibWeb/WebDriver/ExecuteScript.cpp b/Userland/Libraries/LibWeb/WebDriver/ExecuteScript.cpp index adaa72ebc64..0a8a7cbd91f 100644 --- a/Userland/Libraries/LibWeb/WebDriver/ExecuteScript.cpp +++ b/Userland/Libraries/LibWeb/WebDriver/ExecuteScript.cpp @@ -1,289 +1,26 @@ /* * Copyright (c) 2022-2023, Linus Groh + * Copyright (c) 2024, Tim Flynn * * SPDX-License-Identifier: BSD-2-Clause */ -#include -#include -#include -#include -#include -#include -#include #include -#include #include #include -#include #include -#include #include -#include #include -#include -#include -#include -#include #include -#include -#include -#include #include #include #include -#include -#include #include -#include -#include #include #include namespace Web::WebDriver { -#define TRY_OR_JS_ERROR(expression) \ - ({ \ - auto&& _temporary_result = (expression); \ - if (_temporary_result.is_error()) [[unlikely]] \ - return ExecuteScriptResultType::JavaScriptError; \ - static_assert(!::AK::Detail::IsLvalueReference, \ - "Do not return a reference from a fallible expression"); \ - _temporary_result.release_value(); \ - }) - -using SeenMap = HashTable>; - -static ErrorOr internal_json_clone(JS::Realm&, HTML::BrowsingContext const&, JS::Value, SeenMap& seen); -static ErrorOr clone_an_object(JS::Realm&, HTML::BrowsingContext const&, JS::Object const&, SeenMap& seen, auto const& clone_algorithm); - -// https://w3c.github.io/webdriver/#dfn-collection -static bool is_collection(JS::Object const& value) -{ - // A collection is an Object that implements the Iterable interface, and whose: - return ( - // - initial value of the toString own property is "Arguments" - value.has_parameter_map() - // - instance of Array - || is(value) - // - instance of DOMTokenList - || is(value) - // - instance of FileList - || is(value) - // - instance of HTMLAllCollection - || is(value) - // - instance of HTMLCollection - || is(value) - // - instance of HTMLFormControlsCollection - || is(value) - // - instance of HTMLOptionsCollection - || is(value) - // - instance of NodeList - || is(value)); -} - -// https://w3c.github.io/webdriver/#dfn-json-clone -static ErrorOr json_clone(JS::Realm& realm, HTML::BrowsingContext const& browsing_context, JS::Value value) -{ - // To JSON clone given session and value, return the result of internal JSON clone with session, value and an empty List. - SeenMap seen; - return internal_json_clone(realm, browsing_context, value, seen); -} - -// https://w3c.github.io/webdriver/#dfn-internal-json-clone -static ErrorOr internal_json_clone(JS::Realm& realm, HTML::BrowsingContext const& browsing_context, JS::Value value, SeenMap& seen) -{ - auto& vm = realm.vm(); - - // To internal JSON clone given session, value and seen, return the value of the first matching statement, matching - // on value: - - // -> undefined - // -> null - if (value.is_nullish()) { - // Return success with data null. - return JsonValue {}; - } - - // -> type Boolean - // -> type Number - // -> type String - // Return success with data value. - if (value.is_boolean()) - return JsonValue { value.as_bool() }; - if (value.is_number()) - return JsonValue { value.as_double() }; - if (value.is_string()) - return JsonValue { value.as_string().byte_string() }; - - // AD-HOC: BigInt and Symbol not mentioned anywhere in the WebDriver spec, as it references ES5. - // It assumes that all primitives are handled above, and the value is an object for the remaining steps. - if (value.is_bigint() || value.is_symbol()) - return ExecuteScriptResultType::JavaScriptError; - - VERIFY(value.is_object()); - auto const& object = static_cast(value.as_object()); - - // -> instance of Element - if (is(object)) { - auto const& element = static_cast(object); - - // If the element is stale, return error with error code stale element reference. - if (is_element_stale(element)) { - return ExecuteScriptResultType::StaleElement; - } - // Otherwise: - else { - // 1. Let reference be the web element reference object for session and value. - auto reference = web_element_reference_object(browsing_context, element); - - // 2. Return success with data reference. - return reference; - } - } - - // -> instance of ShadowRoot - if (is(object)) { - auto const& shadow_root = static_cast(object); - - // If the shadow root is detached, return error with error code detached shadow root. - if (is_shadow_root_detached(shadow_root)) { - return ExecuteScriptResultType::DetachedShadowRoot; - } - // Otherwise: - else { - // 1. Let reference be the shadow root reference object for session and value. - auto reference = shadow_root_reference_object(browsing_context, shadow_root); - - // 2. Return success with data reference. - return reference; - } - } - - // -> a WindowProxy object - if (is(object)) { - auto const& window_proxy = static_cast(object); - - // If the associated browsing context of the WindowProxy object in value has been destroyed, return error - // with error code stale element reference. - if (window_proxy.associated_browsing_context()->has_navigable_been_destroyed()) { - return ExecuteScriptResultType::BrowsingContextDiscarded; - } - // Otherwise: - else { - // 1. Let reference be the WindowProxy reference object for value. - auto reference = window_proxy_reference_object(window_proxy); - - // 2. Return success with data reference. - return reference; - } - } - - // -> has an own property named "toJSON" that is a Function - if (auto to_json = object.get_without_side_effects(vm.names.toJSON); to_json.is_function()) { - // Return success with the value returned by Function.[[Call]](toJSON) with value as the this value. - auto to_json_result = TRY_OR_JS_ERROR(to_json.as_function().internal_call(value, JS::MarkedVector { vm.heap() })); - if (!to_json_result.is_string()) - return ExecuteScriptResultType::JavaScriptError; - - return to_json_result.as_string().byte_string(); - } - - // -> Otherwise - // 1. Let result be clone an object with session value and seen, and internal JSON clone as the clone algorithm. - auto result = TRY(clone_an_object(realm, browsing_context, object, seen, internal_json_clone)); - - // 2. Return success with data result. - return result; -} - -// https://w3c.github.io/webdriver/#dfn-clone-an-object -static ErrorOr clone_an_object(JS::Realm& realm, HTML::BrowsingContext const& browsing_context, JS::Object const& value, SeenMap& seen, auto const& clone_algorithm) -{ - auto& vm = realm.vm(); - - // 1. If value is in seen, return error with error code javascript error. - if (seen.contains(value)) - return ExecuteScriptResultType::JavaScriptError; - - // 2. Append value to seen. - seen.set(value); - - // 3. Let result be the value of the first matching statement, matching on value: - auto result = TRY(([&]() -> ErrorOr, ExecuteScriptResultType> { - // -> a collection - if (is_collection(value)) { - // A new Array which length property is equal to the result of getting the property length of value. - auto length_property = TRY_OR_JS_ERROR(value.get(vm.names.length)); - - auto length = TRY_OR_JS_ERROR(length_property.to_length(vm)); - if (length > NumericLimits::max()) - return ExecuteScriptResultType::JavaScriptError; - - auto array = JsonArray {}; - for (size_t i = 0; i < length; ++i) - array.must_append(JsonValue {}); - - return array; - } - // -> Otherwise - else { - // A new Object. - return JsonObject {}; - } - }())); - - Optional error; - - // 4. For each enumerable property in value, run the following substeps: - (void)value.enumerate_object_properties([&](auto property) -> Optional { - // 1. Let name be the name of the property. - auto name = MUST(JS::PropertyKey::from_value(vm, property)); - - // 2. Let source property value be the result of getting a property named name from value. If doing so causes - // script to be run and that script throws an error, return error with error code javascript error. - auto source_property_value = value.get(name); - if (source_property_value.is_error()) { - error = ExecuteScriptResultType::JavaScriptError; - return JS::normal_completion({}); - } - - // 3. Let cloned property result be the result of calling the clone algorithm with session, source property - // value and seen. - auto cloned_property_result = clone_algorithm(realm, browsing_context, source_property_value.value(), seen); - - // 4. If cloned property result is a success, set a property of result with name name and value equal to cloned - // property result's data. - if (!cloned_property_result.is_error()) { - result.visit( - [&](JsonArray& array) { - // NOTE: If this was a JS array, only indexed properties would be serialized anyway. - if (name.is_number()) - array.set(name.as_number(), cloned_property_result.value()); - }, - [&](JsonObject& object) { - object.set(name.to_string(), cloned_property_result.value()); - }); - } - // 5. Otherwise, return cloned property result. - else { - error = cloned_property_result.release_error(); - return JS::normal_completion({}); - } - - return {}; - }); - - if (error.has_value()) - return error.release_value(); - - // 5. Remove the last element of seen. - seen.remove(value); - - // 6. Return success with data result. - return result.visit([&](auto& value) { return JsonValue { move(value) }; }); -} - // https://w3c.github.io/webdriver/#dfn-execute-a-function-body JS::ThrowCompletionOr execute_a_function_body(HTML::BrowsingContext const& browsing_context, ByteString const& body, ReadonlySpan parameters) { @@ -358,15 +95,6 @@ JS::ThrowCompletionOr execute_a_function_body(HTML::Window const& win return completion; } -static ExecuteScriptResultSerialized create_timeout_result() -{ - JsonObject error_object; - error_object.set("name", "Error"); - error_object.set("message", "Script Timeout"); - - return { ExecuteScriptResultType::Timeout, move(error_object) }; -} - void execute_script(HTML::BrowsingContext const& browsing_context, ByteString body, JS::MarkedVector arguments, Optional const& timeout_ms, JS::NonnullGCPtr on_complete) { auto const* document = browsing_context.active_document(); @@ -380,7 +108,7 @@ void execute_script(HTML::BrowsingContext const& browsing_context, ByteString bo if (timeout_ms.has_value()) { // 1. Start the timer with timer and timeout. timer->start(timeout_ms.value(), JS::create_heap_function(vm.heap(), [on_complete]() { - on_complete->function()(create_timeout_result()); + on_complete->function()({ .state = JS::Promise::State::Pending }); })); } @@ -410,34 +138,13 @@ void execute_script(HTML::BrowsingContext const& browsing_context, ByteString bo })); // 9. Wait until promise is resolved, or timer's timeout fired flag is set, whichever occurs first. - auto reaction_steps = JS::create_heap_function(vm.heap(), [&realm, &browsing_context, promise, timer, on_complete](JS::Value) -> WebIDL::ExceptionOr { + auto reaction_steps = JS::create_heap_function(vm.heap(), [promise, timer, on_complete](JS::Value) -> WebIDL::ExceptionOr { if (timer->is_timed_out()) return JS::js_undefined(); timer->stop(); auto promise_promise = JS::NonnullGCPtr { verify_cast(*promise->promise()) }; - - auto json_value_or_error = json_clone(realm, browsing_context, promise_promise->result()); - if (json_value_or_error.is_error()) { - auto error_object = JsonObject {}; - error_object.set("name", "Error"); - error_object.set("message", "Could not clone result value"); - - on_complete->function()({ ExecuteScriptResultType::JavaScriptError, move(error_object) }); - } - - // 10. If promise is still pending and timer's timeout fired flag is set, return error with error code script timeout. - // NOTE: This is handled by the HeapTimer. - - // 11. If promise is fulfilled with value v, let result be JSON clone with session and v, and return success with data result. - else if (promise_promise->state() == JS::Promise::State::Fulfilled) { - on_complete->function()({ ExecuteScriptResultType::PromiseResolved, json_value_or_error.release_value() }); - } - - // 12. If promise is rejected with reason r, let result be JSON clone with session and r, and return error with error code javascript error and data result. - else if (promise_promise->state() == JS::Promise::State::Rejected) { - on_complete->function()({ ExecuteScriptResultType::PromiseRejected, json_value_or_error.release_value() }); - } + on_complete->function()({ promise_promise->state(), promise_promise->result() }); return JS::js_undefined(); }); @@ -458,7 +165,7 @@ void execute_async_script(HTML::BrowsingContext const& browsing_context, ByteStr if (timeout_ms.has_value()) { // 1. Start the timer with timer and timeout. timer->start(timeout_ms.value(), JS::create_heap_function(vm.heap(), [on_complete]() { - on_complete->function()(create_timeout_result()); + on_complete->function()({ .state = JS::Promise::State::Pending }); })); } @@ -529,33 +236,12 @@ void execute_async_script(HTML::BrowsingContext const& browsing_context, ByteStr })); // 9. Wait until promise is resolved, or timer's timeout fired flag is set, whichever occurs first. - auto reaction_steps = JS::create_heap_function(vm.heap(), [&realm, &browsing_context, promise, timer, on_complete](JS::Value) -> WebIDL::ExceptionOr { + auto reaction_steps = JS::create_heap_function(vm.heap(), [promise, timer, on_complete](JS::Value) -> WebIDL::ExceptionOr { if (timer->is_timed_out()) return JS::js_undefined(); timer->stop(); - auto json_value_or_error = json_clone(realm, browsing_context, promise->result()); - if (json_value_or_error.is_error()) { - auto error_object = JsonObject {}; - error_object.set("name", "Error"); - error_object.set("message", "Could not clone result value"); - - on_complete->function()({ ExecuteScriptResultType::JavaScriptError, move(error_object) }); - } - - // 10. If promise is still pending and timer's timeout fired flag is set, return error with error code script timeout. - // NOTE: This is handled by the HeapTimer. - - // 11. If promise is fulfilled with value v, let result be JSON clone with session and v, and return success with data result. - else if (promise->state() == JS::Promise::State::Fulfilled) { - on_complete->function()({ ExecuteScriptResultType::PromiseResolved, json_value_or_error.release_value() }); - } - - // 12. If promise is rejected with reason r, let result be JSON clone with session and r, and return error with error code javascript error and data result. - else if (promise->state() == JS::Promise::State::Rejected) { - on_complete->function()({ ExecuteScriptResultType::PromiseRejected, json_value_or_error.release_value() }); - } - + on_complete->function()({ promise->state(), promise->result() }); return JS::js_undefined(); }); diff --git a/Userland/Libraries/LibWeb/WebDriver/ExecuteScript.h b/Userland/Libraries/LibWeb/WebDriver/ExecuteScript.h index 2649bce043d..982221ae9dd 100644 --- a/Userland/Libraries/LibWeb/WebDriver/ExecuteScript.h +++ b/Userland/Libraries/LibWeb/WebDriver/ExecuteScript.h @@ -1,5 +1,6 @@ /* * Copyright (c) 2022-2023, Linus Groh + * Copyright (c) 2024, Tim Flynn * * SPDX-License-Identifier: BSD-2-Clause */ @@ -7,35 +8,20 @@ #pragma once #include -#include #include #include +#include #include #include namespace Web::WebDriver { -enum class ExecuteScriptResultType { - PromiseResolved, - PromiseRejected, - Timeout, - JavaScriptError, - BrowsingContextDiscarded, - StaleElement, - DetachedShadowRoot, +struct ExecutionResult { + JS::Promise::State state { JS::Promise::State::Pending }; + JS::Value value {}; }; -struct ExecuteScriptResult { - ExecuteScriptResultType type; - JS::Value value; -}; - -struct ExecuteScriptResultSerialized { - ExecuteScriptResultType type; - JsonValue value; -}; - -using OnScriptComplete = JS::HeapFunction; +using OnScriptComplete = JS::HeapFunction; JS::ThrowCompletionOr execute_a_function_body(HTML::BrowsingContext const&, ByteString const& body, ReadonlySpan parameters); JS::ThrowCompletionOr execute_a_function_body(HTML::Window const&, ByteString const& body, ReadonlySpan parameters, JS::GCPtr environment_override_object = {}); diff --git a/Userland/Libraries/LibWeb/WebDriver/JSON.cpp b/Userland/Libraries/LibWeb/WebDriver/JSON.cpp new file mode 100644 index 00000000000..efda7594e9f --- /dev/null +++ b/Userland/Libraries/LibWeb/WebDriver/JSON.cpp @@ -0,0 +1,264 @@ +/* + * Copyright (c) 2022-2023, Linus Groh + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Web::WebDriver { + +#define TRY_OR_JS_ERROR(expression) \ + ({ \ + auto&& _temporary_result = (expression); \ + if (_temporary_result.is_error()) [[unlikely]] \ + return WebDriver::Error::from_code(ErrorCode::JavascriptError, "Script returned an error"); \ + static_assert(!::AK::Detail::IsLvalueReference, \ + "Do not return a reference from a fallible expression"); \ + _temporary_result.release_value(); \ + }) + +using SeenMap = HashTable>; + +// https://w3c.github.io/webdriver/#dfn-collection +static bool is_collection(JS::Object const& value) +{ + // A collection is an Object that implements the Iterable interface, and whose: + return ( + // - initial value of the toString own property is "Arguments" + value.has_parameter_map() + // - instance of Array + || is(value) + // - instance of DOMTokenList + || is(value) + // - instance of FileList + || is(value) + // - instance of HTMLAllCollection + || is(value) + // - instance of HTMLCollection + || is(value) + // - instance of HTMLFormControlsCollection + || is(value) + // - instance of HTMLOptionsCollection + || is(value) + // - instance of NodeList + || is(value)); +} + +// https://w3c.github.io/webdriver/#dfn-clone-an-object +static Response clone_an_object(HTML::BrowsingContext const& browsing_context, JS::Object const& value, SeenMap& seen, auto const& clone_algorithm) +{ + auto& vm = browsing_context.vm(); + + // 1. If value is in seen, return error with error code javascript error. + if (seen.contains(value)) + return WebDriver::Error::from_code(ErrorCode::JavascriptError, "Attempted to recursively clone an Object"sv); + + // 2. Append value to seen. + seen.set(value); + + // 3. Let result be the value of the first matching statement, matching on value: + auto result = TRY(([&]() -> Response { + // -> a collection + if (is_collection(value)) { + // A new Array which length property is equal to the result of getting the property length of value. + auto length_property = TRY_OR_JS_ERROR(value.get(vm.names.length)); + + auto length = TRY_OR_JS_ERROR(length_property.to_length(vm)); + if (length > NumericLimits::max()) + return WebDriver::Error::from_code(ErrorCode::JavascriptError, "Length of Object too large"sv); + + return JsonValue { JsonArray { length } }; + } + // -> Otherwise + else { + // A new Object. + return JsonValue { JsonObject {} }; + } + }())); + + Optional error; + + // 4. For each enumerable property in value, run the following substeps: + (void)value.enumerate_object_properties([&](auto property) -> Optional { + // 1. Let name be the name of the property. + auto name = MUST(JS::PropertyKey::from_value(vm, property)); + + // 2. Let source property value be the result of getting a property named name from value. If doing so causes + // script to be run and that script throws an error, return error with error code javascript error. + auto source_property_value = value.get(name); + if (source_property_value.is_error()) { + error = WebDriver::Error::from_code(ErrorCode::JavascriptError, "Script returned an error"); + return JS::normal_completion({}); + } + + // 3. Let cloned property result be the result of calling the clone algorithm with session, source property + // value and seen. + auto cloned_property_result = clone_algorithm(browsing_context, source_property_value.value(), seen); + + // 4. If cloned property result is a success, set a property of result with name name and value equal to cloned + // property result's data. + if (!cloned_property_result.is_error()) { + if (result.is_array() && name.is_number()) + result.as_array().set(name.as_number(), cloned_property_result.value()); + else if (result.is_object()) + result.as_object().set(name.to_string(), cloned_property_result.value()); + } + // 5. Otherwise, return cloned property result. + else { + error = cloned_property_result.release_error(); + return JS::normal_completion({}); + } + + return {}; + }); + + if (error.has_value()) + return error.release_value(); + + // 5. Remove the last element of seen. + seen.remove(value); + + // 6. Return success with data result. + return result; +} + +// https://w3c.github.io/webdriver/#dfn-internal-json-clone +static Response internal_json_clone(HTML::BrowsingContext const& browsing_context, JS::Value value, SeenMap& seen) +{ + auto& vm = browsing_context.vm(); + + // To internal JSON clone given session, value and seen, return the value of the first matching statement, matching + // on value: + + // -> undefined + // -> null + if (value.is_nullish()) { + // Return success with data null. + return JsonValue {}; + } + + // -> type Boolean + // -> type Number + // -> type String + // Return success with data value. + if (value.is_boolean()) + return JsonValue { value.as_bool() }; + if (value.is_number()) + return JsonValue { value.as_double() }; + if (value.is_string()) + return JsonValue { value.as_string().byte_string() }; + + // AD-HOC: BigInt and Symbol not mentioned anywhere in the WebDriver spec, as it references ES5. + // It assumes that all primitives are handled above, and the value is an object for the remaining steps. + if (value.is_bigint()) + return WebDriver::Error::from_code(ErrorCode::JavascriptError, "Cannot clone a BigInt"sv); + if (value.is_symbol()) + return WebDriver::Error::from_code(ErrorCode::JavascriptError, "Cannot clone a Symbol"sv); + + VERIFY(value.is_object()); + auto const& object = static_cast(value.as_object()); + + // -> instance of Element + if (is(object)) { + auto const& element = static_cast(object); + + // If the element is stale, return error with error code stale element reference. + if (is_element_stale(element)) { + return WebDriver::Error::from_code(ErrorCode::StaleElementReference, "Referenced element has become stale"sv); + } + // Otherwise: + else { + // 1. Let reference be the web element reference object for session and value. + auto reference = web_element_reference_object(browsing_context, element); + + // 2. Return success with data reference. + return JsonValue { move(reference) }; + } + } + + // -> instance of ShadowRoot + if (is(object)) { + auto const& shadow_root = static_cast(object); + + // If the shadow root is detached, return error with error code detached shadow root. + if (is_shadow_root_detached(shadow_root)) { + return WebDriver::Error::from_code(ErrorCode::DetachedShadowRoot, "Referenced shadow root has become detached"sv); + } + // Otherwise: + else { + // 1. Let reference be the shadow root reference object for session and value. + auto reference = shadow_root_reference_object(browsing_context, shadow_root); + + // 2. Return success with data reference. + return JsonValue { move(reference) }; + } + } + + // -> a WindowProxy object + if (is(object)) { + auto const& window_proxy = static_cast(object); + + // If the associated browsing context of the WindowProxy object in value has been destroyed, return error + // with error code stale element reference. + if (window_proxy.associated_browsing_context()->has_navigable_been_destroyed()) { + return WebDriver::Error::from_code(ErrorCode::StaleElementReference, "Browsing context has been discarded"sv); + } + // Otherwise: + else { + // 1. Let reference be the WindowProxy reference object for value. + auto reference = window_proxy_reference_object(window_proxy); + + // 2. Return success with data reference. + return JsonValue { move(reference) }; + } + } + + // -> has an own property named "toJSON" that is a Function + if (auto to_json = object.get_without_side_effects(vm.names.toJSON); to_json.is_function()) { + // Return success with the value returned by Function.[[Call]](toJSON) with value as the this value. + auto to_json_result = TRY_OR_JS_ERROR(to_json.as_function().internal_call(value, JS::MarkedVector { vm.heap() })); + if (!to_json_result.is_string()) + return WebDriver::Error::from_code(ErrorCode::JavascriptError, "toJSON did not return a String"sv); + + return JsonValue { to_json_result.as_string().byte_string() }; + } + + // -> Otherwise + // 1. Let result be clone an object with session value and seen, and internal JSON clone as the clone algorithm. + auto result = TRY(clone_an_object(browsing_context, object, seen, internal_json_clone)); + + // 2. Return success with data result. + return result; +} + +// https://w3c.github.io/webdriver/#dfn-json-clone +Response json_clone(HTML::BrowsingContext const& browsing_context, JS::Value value) +{ + SeenMap seen; + + // To JSON clone given session and value, return the result of internal JSON clone with session, value and an empty List. + return internal_json_clone(browsing_context, value, seen); +} + +} diff --git a/Userland/Libraries/LibWeb/WebDriver/JSON.h b/Userland/Libraries/LibWeb/WebDriver/JSON.h new file mode 100644 index 00000000000..ccc7a8125a5 --- /dev/null +++ b/Userland/Libraries/LibWeb/WebDriver/JSON.h @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2022-2023, Linus Groh + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include + +namespace Web::WebDriver { + +Response json_clone(HTML::BrowsingContext const&, JS::Value); + +} diff --git a/Userland/Services/WebContent/WebDriverConnection.cpp b/Userland/Services/WebContent/WebDriverConnection.cpp index 4f8af134bd9..4d23af3ff8c 100644 --- a/Userland/Services/WebContent/WebDriverConnection.cpp +++ b/Userland/Services/WebContent/WebDriverConnection.cpp @@ -56,6 +56,7 @@ #include #include #include +#include #include #include #include @@ -1888,9 +1889,9 @@ Messages::WebDriverClient::ExecuteScriptResponse WebDriverConnection::execute_sc auto timeout_ms = m_timeouts_configuration.script_timeout; // This handles steps 5 to 9 and produces the appropriate result type for the following steps. - Web::WebDriver::execute_script(current_browsing_context(), move(body), move(arguments), timeout_ms, JS::create_heap_function(current_browsing_context().heap(), [this](Web::WebDriver::ExecuteScriptResultSerialized result) { + Web::WebDriver::execute_script(current_browsing_context(), move(body), move(arguments), timeout_ms, JS::create_heap_function(current_browsing_context().heap(), [this](Web::WebDriver::ExecutionResult result) { dbgln_if(WEBDRIVER_DEBUG, "Executing script returned: {}", result.value); - handle_script_response(move(result)); + handle_script_response(result); })); }); @@ -1917,41 +1918,39 @@ Messages::WebDriverClient::ExecuteAsyncScriptResponse WebDriverConnection::execu auto timeout_ms = m_timeouts_configuration.script_timeout; // This handles steps 5 to 9 and produces the appropriate result type for the following steps. - Web::WebDriver::execute_async_script(current_browsing_context(), move(body), move(arguments), timeout_ms, JS::create_heap_function(current_browsing_context().heap(), [&](Web::WebDriver::ExecuteScriptResultSerialized result) { + Web::WebDriver::execute_async_script(current_browsing_context(), move(body), move(arguments), timeout_ms, JS::create_heap_function(current_browsing_context().heap(), [&](Web::WebDriver::ExecutionResult result) { dbgln_if(WEBDRIVER_DEBUG, "Executing async script returned: {}", result.value); - handle_script_response(move(result)); + handle_script_response(result); })); }); return JsonValue {}; } -void WebDriverConnection::handle_script_response(Web::WebDriver::ExecuteScriptResultSerialized result) +void WebDriverConnection::handle_script_response(Web::WebDriver::ExecutionResult result) { if (!m_has_pending_script_execution) return; m_has_pending_script_execution = false; auto response = [&]() -> Web::WebDriver::Response { - switch (result.type) { - // 10. If promise is still pending and the session script timeout is reached, return error with error code script timeout. - case Web::WebDriver::ExecuteScriptResultType::Timeout: + switch (result.state) { + // 10. If promise is still pending and timer's timeout fired flag is set, return error with error code script + // timeout. + case JS::Promise::State::Pending: return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::ScriptTimeoutError, "Script timed out"); - // 11. Upon fulfillment of promise with value v, let result be a JSON clone of v, and return success with data result. - case Web::WebDriver::ExecuteScriptResultType::PromiseResolved: - return move(result.value); + // 11. If promise is fulfilled with value v, let result be JSON clone with session and v, and return success + // with data result. + case JS::Promise::State::Fulfilled: + return Web::WebDriver::json_clone(current_browsing_context(), result.value); - // 12. Upon rejection of promise with reason r, let result be a JSON clone of r, and return error with error code javascript error and data result. - case Web::WebDriver::ExecuteScriptResultType::PromiseRejected: - case Web::WebDriver::ExecuteScriptResultType::JavaScriptError: - return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::JavascriptError, "Script returned an error", move(result.value)); - case Web::WebDriver::ExecuteScriptResultType::BrowsingContextDiscarded: - return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::StaleElementReference, "Browsing context has been discarded", move(result.value)); - case Web::WebDriver::ExecuteScriptResultType::StaleElement: - return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::StaleElementReference, "Referenced element has become stale", move(result.value)); - case Web::WebDriver::ExecuteScriptResultType::DetachedShadowRoot: - return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::DetachedShadowRoot, "Referenced shadow root has become detached", move(result.value)); + // 12. If promise is rejected with reason r, let result be JSON clone with session and r, and return error + // with error code javascript error and data result. + case JS::Promise::State::Rejected: { + auto reason = TRY(Web::WebDriver::json_clone(current_browsing_context(), result.value)); + return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::JavascriptError, "Script returned an error", move(reason)); + } } VERIFY_NOT_REACHED(); diff --git a/Userland/Services/WebContent/WebDriverConnection.h b/Userland/Services/WebContent/WebDriverConnection.h index 223354d233c..a1c26512625 100644 --- a/Userland/Services/WebContent/WebDriverConnection.h +++ b/Userland/Services/WebContent/WebDriverConnection.h @@ -148,7 +148,7 @@ private: JS::MarkedVector arguments; }; ErrorOr extract_the_script_arguments_from_a_request(JS::VM&, JsonValue const& payload); - void handle_script_response(Web::WebDriver::ExecuteScriptResultSerialized); + void handle_script_response(Web::WebDriver::ExecutionResult); void delete_cookies(Optional const& name = {});