diff --git a/components/script/dom/htmlelement.rs b/components/script/dom/htmlelement.rs index d3a6d2d5dc9..b5edc6bd7c9 100644 --- a/components/script/dom/htmlelement.rs +++ b/components/script/dom/htmlelement.rs @@ -1259,7 +1259,6 @@ impl FormControl for HTMLElement { } fn to_element(&self) -> &Element { - debug_assert!(self.is_form_associated_custom_element()); self.as_element() } @@ -1268,5 +1267,5 @@ impl FormControl for HTMLElement { true } - // TODO candidate_for_validation, satisfies_constraints traits + // TODO satisfies_constraints traits } diff --git a/components/script/dom/htmlformelement.rs b/components/script/dom/htmlformelement.rs index 00377fd8208..4a186530ed2 100644 --- a/components/script/dom/htmlformelement.rs +++ b/components/script/dom/htmlformelement.rs @@ -1698,8 +1698,18 @@ pub(crate) trait FormControl: DomObject { } } + /// + fn is_candidate_for_constraint_validation(&self) -> bool { + let element = self.to_element(); + let html_element = element.downcast::(); + if let Some(html_element) = html_element { + html_element.is_submittable_element() || element.is_instance_validatable() + } else { + false + } + } + // XXXKiChjang: Implement these on inheritors - // fn candidate_for_validation(&self) -> bool; // fn satisfies_constraints(&self) -> bool; } diff --git a/components/script/dom/htmlinputelement.rs b/components/script/dom/htmlinputelement.rs index 7495f6eab78..090190b01f7 100644 --- a/components/script/dom/htmlinputelement.rs +++ b/components/script/dom/htmlinputelement.rs @@ -2193,6 +2193,26 @@ impl HTMLInputElement { self.upcast::().dirty(NodeDamage::Other); } + /// + /// Used by WebDriver to clear the input element. + pub(crate) fn clear(&self, can_gc: CanGc) { + // Step 1. Reset dirty value and dirty checkedness flags. + self.value_dirty.set(false); + self.checked_changed.set(false); + // Step 2. Set value to empty string. + self.textinput.borrow_mut().set_content(DOMString::from("")); + // Step 3. Set checkedness based on presence of content attribute. + self.update_checked_state(self.DefaultChecked(), false); + self.value_changed(can_gc); + // Step 4. Empty selected files + self.filelist.set(None); + // Step 5. invoke the value sanitization algorithm iff + // the type attribute's current state defines one. + // This is covered in `fn sanitize_value` called below. + self.enable_sanitization(); + self.upcast::().dirty(NodeDamage::Other); + } + fn update_placeholder_shown_state(&self) { if !self.input_type().is_textual_or_password() { return; diff --git a/components/script/dom/htmltextareaelement.rs b/components/script/dom/htmltextareaelement.rs index d3856fa04f3..ab129f9418f 100644 --- a/components/script/dom/htmltextareaelement.rs +++ b/components/script/dom/htmltextareaelement.rs @@ -204,7 +204,7 @@ impl HTMLTextAreaElement { } // https://html.spec.whatwg.org/multipage/#concept-fe-mutable - fn is_mutable(&self) -> bool { + pub(crate) fn is_mutable(&self) -> bool { // https://html.spec.whatwg.org/multipage/#the-textarea-element%3Aconcept-fe-mutable // https://html.spec.whatwg.org/multipage/#the-readonly-attribute:concept-fe-mutable !(self.upcast::().disabled_state() || self.ReadOnly()) @@ -450,6 +450,13 @@ impl HTMLTextAreaElementMethods for HTMLTextAreaElement { } impl HTMLTextAreaElement { + /// + /// Used by WebDriver to clear the textarea element. + pub(crate) fn clear(&self) { + self.value_dirty.set(false); + self.textinput.borrow_mut().set_content(DOMString::from("")); + } + pub(crate) fn reset(&self) { // https://html.spec.whatwg.org/multipage/#the-textarea-element:concept-form-reset-control let mut textinput = self.textinput.borrow_mut(); diff --git a/components/script/script_thread.rs b/components/script/script_thread.rs index 30a4b0e0cb0..521a28721a6 100644 --- a/components/script/script_thread.rs +++ b/components/script/script_thread.rs @@ -2250,6 +2250,15 @@ impl ScriptThread { WebDriverScriptCommand::DeleteCookie(name, reply) => { webdriver_handlers::handle_delete_cookie(&documents, pipeline_id, name, reply) }, + WebDriverScriptCommand::ElementClear(element_id, reply) => { + webdriver_handlers::handle_element_clear( + &documents, + pipeline_id, + element_id, + reply, + can_gc, + ) + }, WebDriverScriptCommand::FindElementsCSSSelector(selector, reply) => { webdriver_handlers::handle_find_elements_css_selector( &documents, diff --git a/components/script/webdriver_handlers.rs b/components/script/webdriver_handlers.rs index 2471d3db471..46fb6231bb9 100644 --- a/components/script/webdriver_handlers.rs +++ b/components/script/webdriver_handlers.rs @@ -41,6 +41,7 @@ use crate::dom::bindings::codegen::Bindings::HTMLElementBinding::HTMLElementMeth use crate::dom::bindings::codegen::Bindings::HTMLInputElementBinding::HTMLInputElementMethods; use crate::dom::bindings::codegen::Bindings::HTMLOptionElementBinding::HTMLOptionElementMethods; use crate::dom::bindings::codegen::Bindings::HTMLSelectElementBinding::HTMLSelectElementMethods; +use crate::dom::bindings::codegen::Bindings::HTMLTextAreaElementBinding::HTMLTextAreaElementMethods; use crate::dom::bindings::codegen::Bindings::NodeBinding::{GetRootNodeOptions, NodeMethods}; use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods; use crate::dom::bindings::codegen::Bindings::XMLSerializerBinding::XMLSerializerMethods; @@ -64,11 +65,13 @@ use crate::dom::globalscope::GlobalScope; use crate::dom::htmlbodyelement::HTMLBodyElement; use crate::dom::htmldatalistelement::HTMLDataListElement; use crate::dom::htmlelement::HTMLElement; +use crate::dom::htmlformelement::FormControl; use crate::dom::htmliframeelement::HTMLIFrameElement; use crate::dom::htmlinputelement::{HTMLInputElement, InputType}; use crate::dom::htmloptgroupelement::HTMLOptGroupElement; use crate::dom::htmloptionelement::HTMLOptionElement; use crate::dom::htmlselectelement::HTMLSelectElement; +use crate::dom::htmltextareaelement::HTMLTextAreaElement; use crate::dom::node::{Node, NodeTraits, ShadowIncluding}; use crate::dom::nodelist::NodeList; use crate::dom::types::ShadowRoot; @@ -1651,6 +1654,107 @@ pub(crate) fn handle_get_url( .unwrap(); } +/// +fn element_is_mutable_form_control(element: &Element) -> bool { + if let Some(input_element) = element.downcast::() { + input_element.is_mutable() && + matches!( + input_element.input_type(), + InputType::Text | + InputType::Search | + InputType::Url | + InputType::Tel | + InputType::Email | + InputType::Password | + InputType::Date | + InputType::Month | + InputType::Week | + InputType::Time | + InputType::DatetimeLocal | + InputType::Number | + InputType::Range | + InputType::Color | + InputType::File + ) + } else if let Some(textarea_element) = element.downcast::() { + textarea_element.is_mutable() + } else { + false + } +} + +/// +fn clear_a_resettable_element(element: &Element, can_gc: CanGc) -> Result<(), ErrorStatus> { + let html_element = element + .downcast::() + .ok_or(ErrorStatus::UnknownError)?; + + // Step 1 - 2. if element is a candidate for constraint + // validation and value is empty, abort steps. + if html_element.is_candidate_for_constraint_validation() { + if let Some(input_element) = element.downcast::() { + if input_element.Value().is_empty() { + return Ok(()); + } + } else if let Some(textarea_element) = element.downcast::() { + if textarea_element.Value().is_empty() { + return Ok(()); + } + } + } + + // Step 3. Invoke the focusing steps for the element. + html_element.Focus(can_gc); + + // Step 4. Run clear algorithm for element. + if let Some(input_element) = element.downcast::() { + input_element.clear(can_gc); + } else if let Some(textarea_element) = element.downcast::() { + textarea_element.clear(); + } else { + unreachable!("We have confirm previously that element is mutable form control"); + } + + let event_target = element.upcast::(); + event_target.fire_bubbling_event(atom!("input"), can_gc); + event_target.fire_bubbling_event(atom!("change"), can_gc); + + // Step 5. Run the unfocusing steps for the element. + html_element.Blur(can_gc); + + Ok(()) +} + +/// +pub(crate) fn handle_element_clear( + documents: &DocumentCollection, + pipeline: PipelineId, + element_id: String, + reply: IpcSender>, + can_gc: CanGc, +) { + reply + .send( + get_known_element(documents, pipeline, element_id).and_then(|element| { + // Step 4. If element is not editable, return ErrorStatus::InvalidElementState. + // TODO: editing hosts and content editable elements are not implemented yet, + // hence we currently skip the check + if !element_is_mutable_form_control(&element) { + return Err(ErrorStatus::InvalidElementState); + } + + // TODO: Step 5. Scroll Into View + // TODO: Step 6 - 10 + // Wait until element become interactable and check. + + // Step 11 + // TODO: Clear content editable elements + clear_a_resettable_element(&element, can_gc) + }), + ) + .unwrap(); +} + fn get_option_parent(node: &Node) -> Option> { // Get parent for `