ElementReference.cpp 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. /*
  2. * Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
  3. *
  4. * SPDX-License-Identifier: BSD-2-Clause
  5. */
  6. #include <LibWeb/DOM/Document.h>
  7. #include <LibWeb/DOM/Element.h>
  8. #include <LibWeb/DOM/Node.h>
  9. #include <LibWeb/DOM/ShadowRoot.h>
  10. #include <LibWeb/Geometry/DOMRect.h>
  11. #include <LibWeb/Geometry/DOMRectList.h>
  12. #include <LibWeb/HTML/BrowsingContext.h>
  13. #include <LibWeb/HTML/HTMLBodyElement.h>
  14. #include <LibWeb/HTML/HTMLInputElement.h>
  15. #include <LibWeb/HTML/HTMLTextAreaElement.h>
  16. #include <LibWeb/HTML/TraversableNavigable.h>
  17. #include <LibWeb/Page/Page.h>
  18. #include <LibWeb/Painting/PaintableBox.h>
  19. #include <LibWeb/WebDriver/ElementReference.h>
  20. namespace Web::WebDriver {
  21. // https://w3c.github.io/webdriver/#dfn-web-element-identifier
  22. static ByteString const web_element_identifier = "element-6066-11e4-a52e-4f735466cecf"sv;
  23. // https://w3c.github.io/webdriver/#dfn-shadow-root-identifier
  24. static ByteString const shadow_root_identifier = "shadow-6066-11e4-a52e-4f735466cecf"sv;
  25. // https://w3c.github.io/webdriver/#dfn-get-or-create-a-web-element-reference
  26. ByteString get_or_create_a_web_element_reference(Web::DOM::Node const& element)
  27. {
  28. // FIXME: 1. For each known element of the current browsing context’s list of known elements:
  29. // FIXME: 1. If known element equals element, return success with known element’s web element reference.
  30. // FIXME: 2. Add element to the list of known elements of the current browsing context.
  31. // FIXME: 3. Return success with the element’s web element reference.
  32. return ByteString::number(element.unique_id());
  33. }
  34. // https://w3c.github.io/webdriver/#dfn-web-element-reference-object
  35. JsonObject web_element_reference_object(Web::DOM::Node const& element)
  36. {
  37. // 1. Let identifier be the web element identifier.
  38. auto identifier = web_element_identifier;
  39. // 2. Let reference be the result of get or create a web element reference given element.
  40. auto reference = get_or_create_a_web_element_reference(element);
  41. // 3. Return a JSON Object initialized with a property with name identifier and value reference.
  42. JsonObject object;
  43. object.set(identifier, reference);
  44. return object;
  45. }
  46. // https://w3c.github.io/webdriver/#dfn-deserialize-a-web-element
  47. ErrorOr<JS::NonnullGCPtr<Web::DOM::Element>, WebDriver::Error> deserialize_web_element(JsonObject const& object)
  48. {
  49. // 1. If object has no own property web element identifier, return error with error code invalid argument.
  50. if (!object.has_string(web_element_identifier))
  51. return WebDriver::Error::from_code(WebDriver::ErrorCode::InvalidArgument, "Object is not a web element");
  52. // 2. Let reference be the result of getting the web element identifier property from object.
  53. auto reference = extract_web_element_reference(object);
  54. // 3. Let element be the result of trying to get a known element with session and reference.
  55. auto* element = TRY(get_known_connected_element(reference));
  56. // 4. Return success with data element.
  57. return *element;
  58. }
  59. ByteString extract_web_element_reference(JsonObject const& object)
  60. {
  61. return object.get_byte_string(web_element_identifier).release_value();
  62. }
  63. // https://w3c.github.io/webdriver/#dfn-represents-a-web-element
  64. bool represents_a_web_element(JsonValue const& value)
  65. {
  66. // An ECMAScript Object represents a web element if it has a web element identifier own property.
  67. if (!value.is_object())
  68. return false;
  69. return value.as_object().has_string(web_element_identifier);
  70. }
  71. // https://w3c.github.io/webdriver/#dfn-get-a-webelement-origin
  72. ErrorOr<JS::NonnullGCPtr<Web::DOM::Element>, Web::WebDriver::Error> get_web_element_origin(StringView origin)
  73. {
  74. // 1. Assert: browsing context is the current browsing context.
  75. // 2. Let element be equal to the result of trying to get a known element with session and origin.
  76. auto* element = TRY(get_known_connected_element(origin));
  77. // 3. Return success with data element.
  78. return *element;
  79. }
  80. // https://w3c.github.io/webdriver/#dfn-get-a-known-element
  81. ErrorOr<Web::DOM::Element*, Web::WebDriver::Error> get_known_connected_element(StringView element_id)
  82. {
  83. // NOTE: The whole concept of "connected elements" is not implemented yet. See get_or_create_a_web_element_reference().
  84. // For now the element is only represented by its ID.
  85. // 1. If not node reference is known with session, session's current browsing context, and reference return error
  86. // with error code no such element.
  87. auto element = element_id.to_number<int>();
  88. if (!element.has_value())
  89. return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::NoSuchElement, "Element ID is not an integer");
  90. // 2. Let node be the result of get a node with session, session's current browsing context, and reference.
  91. auto* node = Web::DOM::Node::from_unique_id(*element);
  92. // 3. If node is not null and node does not implement Element return error with error code no such element.
  93. if (node && !node->is_element())
  94. return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::NoSuchElement, ByteString::formatted("Could not find element with ID: {}", element_id));
  95. // 4. If node is null or node is stale return error with error code stale element reference.
  96. if (!node || is_element_stale(*node))
  97. return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::StaleElementReference, ByteString::formatted("Element with ID: {} is stale", element_id));
  98. // 5. Return success with data node.
  99. return static_cast<Web::DOM::Element*>(node);
  100. }
  101. // https://w3c.github.io/webdriver/#dfn-is-stale
  102. bool is_element_stale(Web::DOM::Node const& element)
  103. {
  104. // An element is stale if its node document is not the active document or if it is not connected.
  105. return !element.document().is_active() || !element.is_connected();
  106. }
  107. // https://w3c.github.io/webdriver/#dfn-interactable
  108. bool is_element_interactable(Web::HTML::BrowsingContext const& browsing_context, Web::DOM::Element const& element)
  109. {
  110. // An interactable element is an element which is either pointer-interactable or keyboard-interactable.
  111. return is_element_keyboard_interactable(element) || is_element_pointer_interactable(browsing_context, element);
  112. }
  113. // https://w3c.github.io/webdriver/#dfn-pointer-interactable
  114. bool is_element_pointer_interactable(Web::HTML::BrowsingContext const& browsing_context, Web::DOM::Element const& element)
  115. {
  116. // A pointer-interactable element is defined to be the first element, defined by the paint order found at the center
  117. // point of its rectangle that is inside the viewport, excluding the size of any rendered scrollbars.
  118. auto const* document = browsing_context.active_document();
  119. if (!document)
  120. return false;
  121. auto const* paint_root = document->paintable_box();
  122. if (!paint_root)
  123. return false;
  124. auto viewport = browsing_context.page().top_level_traversable()->viewport_rect();
  125. auto center_point = in_view_center_point(element, viewport);
  126. auto result = paint_root->hit_test(center_point, Painting::HitTestType::TextCursor);
  127. if (!result.has_value())
  128. return false;
  129. return result->dom_node() == &element;
  130. }
  131. // https://w3c.github.io/webdriver/#dfn-keyboard-interactable
  132. bool is_element_keyboard_interactable(Web::DOM::Element const& element)
  133. {
  134. // A keyboard-interactable element is any element that has a focusable area, is a body element, or is the document element.
  135. return element.is_focusable() || is<HTML::HTMLBodyElement>(element) || element.is_document_element();
  136. }
  137. // https://w3c.github.io/webdriver/#dfn-editable
  138. bool is_element_editable(Web::DOM::Element const& element)
  139. {
  140. // Editable elements are those that can be used for typing and clearing, and they fall into two subcategories:
  141. // "Mutable form control elements" and "Mutable elements".
  142. return is_element_mutable_form_control(element) || is_element_mutable(element);
  143. }
  144. // https://w3c.github.io/webdriver/#dfn-mutable-element
  145. bool is_element_mutable(Web::DOM::Element const& element)
  146. {
  147. // Denotes elements that are editing hosts or content editable.
  148. if (!is<HTML::HTMLElement>(element))
  149. return false;
  150. auto const& html_element = static_cast<HTML::HTMLElement const&>(element);
  151. return html_element.is_editable();
  152. }
  153. // https://w3c.github.io/webdriver/#dfn-mutable-form-control-element
  154. bool is_element_mutable_form_control(Web::DOM::Element const& element)
  155. {
  156. // Denotes input elements that are mutable (e.g. that are not read only or disabled) and whose type attribute is
  157. // in one of the following states:
  158. if (is<HTML::HTMLInputElement>(element)) {
  159. auto const& input_element = static_cast<HTML::HTMLInputElement const&>(element);
  160. if (!input_element.is_mutable() || !input_element.enabled())
  161. return false;
  162. // Text and Search, URL, Telephone, Email, Password, Date, Month, Week, Time, Local Date and Time, Number,
  163. // Range, Color, File Upload
  164. switch (input_element.type_state()) {
  165. case HTML::HTMLInputElement::TypeAttributeState::Text:
  166. case HTML::HTMLInputElement::TypeAttributeState::Search:
  167. case HTML::HTMLInputElement::TypeAttributeState::URL:
  168. case HTML::HTMLInputElement::TypeAttributeState::Telephone:
  169. case HTML::HTMLInputElement::TypeAttributeState::Email:
  170. case HTML::HTMLInputElement::TypeAttributeState::Password:
  171. case HTML::HTMLInputElement::TypeAttributeState::Date:
  172. case HTML::HTMLInputElement::TypeAttributeState::Month:
  173. case HTML::HTMLInputElement::TypeAttributeState::Week:
  174. case HTML::HTMLInputElement::TypeAttributeState::Time:
  175. case HTML::HTMLInputElement::TypeAttributeState::LocalDateAndTime:
  176. case HTML::HTMLInputElement::TypeAttributeState::Number:
  177. case HTML::HTMLInputElement::TypeAttributeState::Range:
  178. case HTML::HTMLInputElement::TypeAttributeState::Color:
  179. case HTML::HTMLInputElement::TypeAttributeState::FileUpload:
  180. return true;
  181. default:
  182. return false;
  183. }
  184. }
  185. // And the textarea element.
  186. if (is<HTML::HTMLTextAreaElement>(element)) {
  187. auto const& text_area = static_cast<HTML::HTMLTextAreaElement const&>(element);
  188. return text_area.enabled();
  189. }
  190. return false;
  191. }
  192. // https://w3c.github.io/webdriver/#dfn-non-typeable-form-control
  193. bool is_element_non_typeable_form_control(Web::DOM::Element const& element)
  194. {
  195. // A non-typeable form control is an input element whose type attribute state causes the primary input mechanism not
  196. // to be through means of a keyboard, whether virtual or physical.
  197. if (!is<HTML::HTMLInputElement>(element))
  198. return false;
  199. auto const& input_element = static_cast<HTML::HTMLInputElement const&>(element);
  200. switch (input_element.type_state()) {
  201. case HTML::HTMLInputElement::TypeAttributeState::Hidden:
  202. case HTML::HTMLInputElement::TypeAttributeState::Range:
  203. case HTML::HTMLInputElement::TypeAttributeState::Color:
  204. case HTML::HTMLInputElement::TypeAttributeState::Checkbox:
  205. case HTML::HTMLInputElement::TypeAttributeState::RadioButton:
  206. case HTML::HTMLInputElement::TypeAttributeState::FileUpload:
  207. case HTML::HTMLInputElement::TypeAttributeState::SubmitButton:
  208. case HTML::HTMLInputElement::TypeAttributeState::ImageButton:
  209. case HTML::HTMLInputElement::TypeAttributeState::ResetButton:
  210. case HTML::HTMLInputElement::TypeAttributeState::Button:
  211. return true;
  212. default:
  213. return false;
  214. }
  215. }
  216. // https://w3c.github.io/webdriver/#dfn-get-or-create-a-shadow-root-reference
  217. ByteString get_or_create_a_shadow_root_reference(Web::DOM::ShadowRoot const& shadow_root)
  218. {
  219. // FIXME: 1. For each known shadow root of the current browsing context’s list of known shadow roots:
  220. // FIXME: 1. If known shadow root equals shadow root, return success with known shadow root’s shadow root reference.
  221. // FIXME: 2. Add shadow to the list of known shadow roots of the current browsing context.
  222. // FIXME: 3. Return success with the shadow’s shadow root reference.
  223. return ByteString::number(shadow_root.unique_id());
  224. }
  225. // https://w3c.github.io/webdriver/#dfn-shadow-root-reference-object
  226. JsonObject shadow_root_reference_object(Web::DOM::ShadowRoot const& shadow_root)
  227. {
  228. // 1. Let identifier be the shadow root identifier.
  229. auto identifier = shadow_root_identifier;
  230. // 2. Let reference be the result of get or create a shadow root reference given shadow root.
  231. auto reference = get_or_create_a_shadow_root_reference(shadow_root);
  232. // 3. Return a JSON Object initialized with a property with name identifier and value reference.
  233. JsonObject object;
  234. object.set(identifier, reference);
  235. return object;
  236. }
  237. // https://w3c.github.io/webdriver/#dfn-get-a-known-shadow-root
  238. ErrorOr<Web::DOM::ShadowRoot*, Web::WebDriver::Error> get_known_shadow_root(StringView shadow_id)
  239. {
  240. // NOTE: The whole concept of "known shadow roots" is not implemented yet. See get_or_create_a_shadow_root_reference().
  241. // For now the shadow root is only represented by its ID.
  242. auto shadow_root = shadow_id.to_number<int>();
  243. if (!shadow_root.has_value())
  244. return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::InvalidArgument, "Shadow ID is not an integer");
  245. auto* node = Web::DOM::Node::from_unique_id(*shadow_root);
  246. if (!node || !node->is_shadow_root())
  247. return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::NoSuchElement, ByteString::formatted("Could not find shadow root with ID: {}", shadow_id));
  248. return static_cast<Web::DOM::ShadowRoot*>(node);
  249. }
  250. // https://w3c.github.io/webdriver/#dfn-center-point
  251. CSSPixelPoint in_view_center_point(DOM::Element const& element, CSSPixelRect viewport)
  252. {
  253. // 1. Let rectangle be the first element of the DOMRect sequence returned by calling getClientRects() on element.
  254. auto const* rectangle = element.get_client_rects()->item(0);
  255. VERIFY(rectangle);
  256. // 2. Let left be max(0, min(x coordinate, x coordinate + width dimension)).
  257. auto left = max(0.0, min(rectangle->x(), rectangle->x() + rectangle->width()));
  258. // 3. Let right be min(innerWidth, max(x coordinate, x coordinate + width dimension)).
  259. auto right = min(viewport.width().to_double(), max(rectangle->x(), rectangle->x() + rectangle->width()));
  260. // 4. Let top be max(0, min(y coordinate, y coordinate + height dimension)).
  261. auto top = max(0.0, min(rectangle->y(), rectangle->y() + rectangle->height()));
  262. // 5. Let bottom be min(innerHeight, max(y coordinate, y coordinate + height dimension)).
  263. auto bottom = min(viewport.height().to_double(), max(rectangle->y(), rectangle->y() + rectangle->height()));
  264. // 6. Let x be floor((left + right) ÷ 2.0).
  265. auto x = floor((left + right) / 2.0);
  266. // 7. Let y be floor((top + bottom) ÷ 2.0).
  267. auto y = floor((top + bottom) / 2.0);
  268. // 8. Return the pair of (x, y).
  269. return { x, y };
  270. }
  271. }