mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2024-11-22 07:30:19 +00:00
LibWeb+WebContent: Simplify hand-rolled script execution result struct
Instead of maintaining a list of script execution result types, which we then map to WebDriver error types, just return the WebDriver error that is specified by the spec. Then perform the JSON clone algorithm from the caller in WebDriverConnection, again as specified by the spec. To do so, this moves the JSON clone algorithm to its own file. This will also be the future home of the JSON deserialize algorithm, which will need some of the internal AOs implemented there.
This commit is contained in:
parent
a5ca036d36
commit
6fb8500a7a
Notes:
github-actions[bot]
2024-11-03 17:08:45 +00:00
Author: https://github.com/trflynn89 Commit: https://github.com/LadybirdBrowser/ladybird/commit/6fb8500a7a8 Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/2137
7 changed files with 317 additions and 363 deletions
|
@ -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
|
||||
|
|
|
@ -1,289 +1,26 @@
|
|||
/*
|
||||
* Copyright (c) 2022-2023, Linus Groh <linusg@serenityos.org>
|
||||
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/JsonArray.h>
|
||||
#include <AK/JsonObject.h>
|
||||
#include <AK/JsonValue.h>
|
||||
#include <AK/NumericLimits.h>
|
||||
#include <AK/ScopeGuard.h>
|
||||
#include <AK/Time.h>
|
||||
#include <AK/Variant.h>
|
||||
#include <LibJS/Parser.h>
|
||||
#include <LibJS/Runtime/Array.h>
|
||||
#include <LibJS/Runtime/ECMAScriptFunctionObject.h>
|
||||
#include <LibJS/Runtime/GlobalEnvironment.h>
|
||||
#include <LibJS/Runtime/JSONObject.h>
|
||||
#include <LibJS/Runtime/ObjectEnvironment.h>
|
||||
#include <LibJS/Runtime/Promise.h>
|
||||
#include <LibJS/Runtime/PromiseConstructor.h>
|
||||
#include <LibWeb/DOM/DOMTokenList.h>
|
||||
#include <LibWeb/DOM/Document.h>
|
||||
#include <LibWeb/DOM/HTMLCollection.h>
|
||||
#include <LibWeb/DOM/NodeList.h>
|
||||
#include <LibWeb/DOM/ShadowRoot.h>
|
||||
#include <LibWeb/FileAPI/FileList.h>
|
||||
#include <LibWeb/HTML/BrowsingContext.h>
|
||||
#include <LibWeb/HTML/HTMLAllCollection.h>
|
||||
#include <LibWeb/HTML/HTMLFormControlsCollection.h>
|
||||
#include <LibWeb/HTML/HTMLOptionsCollection.h>
|
||||
#include <LibWeb/HTML/Scripting/Environments.h>
|
||||
#include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
|
||||
#include <LibWeb/HTML/Window.h>
|
||||
#include <LibWeb/HTML/WindowProxy.h>
|
||||
#include <LibWeb/Page/Page.h>
|
||||
#include <LibWeb/Platform/EventLoopPlugin.h>
|
||||
#include <LibWeb/WebDriver/Contexts.h>
|
||||
#include <LibWeb/WebDriver/ElementReference.h>
|
||||
#include <LibWeb/WebDriver/ExecuteScript.h>
|
||||
#include <LibWeb/WebDriver/HeapTimer.h>
|
||||
|
||||
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<decltype(_temporary_result.release_value())>, \
|
||||
"Do not return a reference from a fallible expression"); \
|
||||
_temporary_result.release_value(); \
|
||||
})
|
||||
|
||||
using SeenMap = HashTable<JS::RawGCPtr<JS::Object const>>;
|
||||
|
||||
static ErrorOr<JsonValue, ExecuteScriptResultType> internal_json_clone(JS::Realm&, HTML::BrowsingContext const&, JS::Value, SeenMap& seen);
|
||||
static ErrorOr<JsonValue, ExecuteScriptResultType> 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<JS::Array>(value)
|
||||
// - instance of DOMTokenList
|
||||
|| is<DOM::DOMTokenList>(value)
|
||||
// - instance of FileList
|
||||
|| is<FileAPI::FileList>(value)
|
||||
// - instance of HTMLAllCollection
|
||||
|| is<HTML::HTMLAllCollection>(value)
|
||||
// - instance of HTMLCollection
|
||||
|| is<DOM::HTMLCollection>(value)
|
||||
// - instance of HTMLFormControlsCollection
|
||||
|| is<HTML::HTMLFormControlsCollection>(value)
|
||||
// - instance of HTMLOptionsCollection
|
||||
|| is<HTML::HTMLOptionsCollection>(value)
|
||||
// - instance of NodeList
|
||||
|| is<DOM::NodeList>(value));
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-json-clone
|
||||
static ErrorOr<JsonValue, ExecuteScriptResultType> 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<JsonValue, ExecuteScriptResultType> 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<JS::Object const&>(value.as_object());
|
||||
|
||||
// -> instance of Element
|
||||
if (is<DOM::Element>(object)) {
|
||||
auto const& element = static_cast<DOM::Element const&>(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<DOM::ShadowRoot>(object)) {
|
||||
auto const& shadow_root = static_cast<DOM::ShadowRoot const&>(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<HTML::WindowProxy>(object)) {
|
||||
auto const& window_proxy = static_cast<HTML::WindowProxy const&>(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<JS::Value> { 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<JsonValue, ExecuteScriptResultType> 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<Variant<JsonArray, JsonObject>, 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<u32>::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<ExecuteScriptResultType> error;
|
||||
|
||||
// 4. For each enumerable property in value, run the following substeps:
|
||||
(void)value.enumerate_object_properties([&](auto property) -> Optional<JS::Completion> {
|
||||
// 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<JS::Value> execute_a_function_body(HTML::BrowsingContext const& browsing_context, ByteString const& body, ReadonlySpan<JS::Value> parameters)
|
||||
{
|
||||
|
@ -358,15 +95,6 @@ JS::ThrowCompletionOr<JS::Value> 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<JS::Value> arguments, Optional<u64> const& timeout_ms, JS::NonnullGCPtr<OnScriptComplete> 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<JS::Value> {
|
||||
auto reaction_steps = JS::create_heap_function(vm.heap(), [promise, timer, on_complete](JS::Value) -> WebIDL::ExceptionOr<JS::Value> {
|
||||
if (timer->is_timed_out())
|
||||
return JS::js_undefined();
|
||||
timer->stop();
|
||||
|
||||
auto promise_promise = JS::NonnullGCPtr { verify_cast<JS::Promise>(*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<JS::Value> {
|
||||
auto reaction_steps = JS::create_heap_function(vm.heap(), [promise, timer, on_complete](JS::Value) -> WebIDL::ExceptionOr<JS::Value> {
|
||||
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();
|
||||
});
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
* Copyright (c) 2022-2023, Linus Groh <linusg@serenityos.org>
|
||||
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
@ -7,35 +8,20 @@
|
|||
#pragma once
|
||||
|
||||
#include <AK/Forward.h>
|
||||
#include <AK/JsonValue.h>
|
||||
#include <LibJS/Forward.h>
|
||||
#include <LibJS/Heap/HeapFunction.h>
|
||||
#include <LibJS/Runtime/Promise.h>
|
||||
#include <LibJS/Runtime/Value.h>
|
||||
#include <LibWeb/Forward.h>
|
||||
|
||||
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<void(ExecuteScriptResultSerialized)>;
|
||||
using OnScriptComplete = JS::HeapFunction<void(ExecutionResult)>;
|
||||
|
||||
JS::ThrowCompletionOr<JS::Value> execute_a_function_body(HTML::BrowsingContext const&, ByteString const& body, ReadonlySpan<JS::Value> parameters);
|
||||
JS::ThrowCompletionOr<JS::Value> execute_a_function_body(HTML::Window const&, ByteString const& body, ReadonlySpan<JS::Value> parameters, JS::GCPtr<JS::Object> environment_override_object = {});
|
||||
|
|
264
Userland/Libraries/LibWeb/WebDriver/JSON.cpp
Normal file
264
Userland/Libraries/LibWeb/WebDriver/JSON.cpp
Normal file
|
@ -0,0 +1,264 @@
|
|||
/*
|
||||
* Copyright (c) 2022-2023, Linus Groh <linusg@serenityos.org>
|
||||
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/HashTable.h>
|
||||
#include <AK/JsonArray.h>
|
||||
#include <AK/JsonObject.h>
|
||||
#include <AK/JsonValue.h>
|
||||
#include <AK/NumericLimits.h>
|
||||
#include <AK/Variant.h>
|
||||
#include <LibJS/Runtime/Array.h>
|
||||
#include <LibWeb/DOM/DOMTokenList.h>
|
||||
#include <LibWeb/DOM/HTMLCollection.h>
|
||||
#include <LibWeb/DOM/NodeList.h>
|
||||
#include <LibWeb/DOM/ShadowRoot.h>
|
||||
#include <LibWeb/FileAPI/FileList.h>
|
||||
#include <LibWeb/HTML/BrowsingContext.h>
|
||||
#include <LibWeb/HTML/HTMLAllCollection.h>
|
||||
#include <LibWeb/HTML/HTMLFormControlsCollection.h>
|
||||
#include <LibWeb/HTML/HTMLOptionsCollection.h>
|
||||
#include <LibWeb/HTML/WindowProxy.h>
|
||||
#include <LibWeb/WebDriver/Contexts.h>
|
||||
#include <LibWeb/WebDriver/ElementReference.h>
|
||||
#include <LibWeb/WebDriver/JSON.h>
|
||||
|
||||
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<decltype(_temporary_result.release_value())>, \
|
||||
"Do not return a reference from a fallible expression"); \
|
||||
_temporary_result.release_value(); \
|
||||
})
|
||||
|
||||
using SeenMap = HashTable<JS::RawGCPtr<JS::Object const>>;
|
||||
|
||||
// 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<JS::Array>(value)
|
||||
// - instance of DOMTokenList
|
||||
|| is<DOM::DOMTokenList>(value)
|
||||
// - instance of FileList
|
||||
|| is<FileAPI::FileList>(value)
|
||||
// - instance of HTMLAllCollection
|
||||
|| is<HTML::HTMLAllCollection>(value)
|
||||
// - instance of HTMLCollection
|
||||
|| is<DOM::HTMLCollection>(value)
|
||||
// - instance of HTMLFormControlsCollection
|
||||
|| is<HTML::HTMLFormControlsCollection>(value)
|
||||
// - instance of HTMLOptionsCollection
|
||||
|| is<HTML::HTMLOptionsCollection>(value)
|
||||
// - instance of NodeList
|
||||
|| is<DOM::NodeList>(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<u32>::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<WebDriver::Error> error;
|
||||
|
||||
// 4. For each enumerable property in value, run the following substeps:
|
||||
(void)value.enumerate_object_properties([&](auto property) -> Optional<JS::Completion> {
|
||||
// 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<JS::Object const&>(value.as_object());
|
||||
|
||||
// -> instance of Element
|
||||
if (is<DOM::Element>(object)) {
|
||||
auto const& element = static_cast<DOM::Element const&>(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<DOM::ShadowRoot>(object)) {
|
||||
auto const& shadow_root = static_cast<DOM::ShadowRoot const&>(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<HTML::WindowProxy>(object)) {
|
||||
auto const& window_proxy = static_cast<HTML::WindowProxy const&>(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<JS::Value> { 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);
|
||||
}
|
||||
|
||||
}
|
18
Userland/Libraries/LibWeb/WebDriver/JSON.h
Normal file
18
Userland/Libraries/LibWeb/WebDriver/JSON.h
Normal file
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Copyright (c) 2022-2023, Linus Groh <linusg@serenityos.org>
|
||||
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <LibJS/Runtime/Value.h>
|
||||
#include <LibWeb/Forward.h>
|
||||
#include <LibWeb/WebDriver/Response.h>
|
||||
|
||||
namespace Web::WebDriver {
|
||||
|
||||
Response json_clone(HTML::BrowsingContext const&, JS::Value);
|
||||
|
||||
}
|
|
@ -56,6 +56,7 @@
|
|||
#include <LibWeb/WebDriver/ElementReference.h>
|
||||
#include <LibWeb/WebDriver/HeapTimer.h>
|
||||
#include <LibWeb/WebDriver/InputState.h>
|
||||
#include <LibWeb/WebDriver/JSON.h>
|
||||
#include <LibWeb/WebDriver/Properties.h>
|
||||
#include <LibWeb/WebDriver/Screenshot.h>
|
||||
#include <WebContent/WebDriverConnection.h>
|
||||
|
@ -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();
|
||||
|
|
|
@ -148,7 +148,7 @@ private:
|
|||
JS::MarkedVector<JS::Value> arguments;
|
||||
};
|
||||
ErrorOr<ScriptArguments, Web::WebDriver::Error> 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<StringView> const& name = {});
|
||||
|
||||
|
|
Loading…
Reference in a new issue