mirror of
https://github.com/lexmount/moli.git
synced 2026-09-29 00:01:38 +00:00
fix(forms): honor range input constraints
This commit is contained in:
@@ -3086,8 +3086,6 @@ html/semantics/forms/form-submission-target/rel-input-target.html
|
||||
html/semantics/forms/textfieldselection/selection.html
|
||||
html/semantics/forms/the-button-element/button-click-submits-with-commandfor.html
|
||||
html/semantics/forms/the-input-element/input-type-button.html
|
||||
html/semantics/forms/the-input-element/range-2.html
|
||||
html/semantics/forms/the-input-element/range.html
|
||||
html/semantics/forms/the-input-element/show-picker-cross-origin-iframe.html
|
||||
html/semantics/forms/the-select-element/customizable-select/base-appearance-inheritance.html
|
||||
html/semantics/forms/the-select-element/customizable-select/option-disabled-optgroup.html
|
||||
|
||||
@@ -6416,6 +6416,8 @@ html/semantics/forms/the-input-element/radio-input-cancel.html
|
||||
html/semantics/forms/the-input-element/radio-morphed.html
|
||||
html/semantics/forms/the-input-element/radio-multiple-selected.html
|
||||
html/semantics/forms/the-input-element/radio.html
|
||||
html/semantics/forms/the-input-element/range-2.html
|
||||
html/semantics/forms/the-input-element/range.html
|
||||
html/semantics/forms/the-input-element/required_attribute.html
|
||||
html/semantics/forms/the-input-element/reset.html
|
||||
html/semantics/forms/the-input-element/search_input.html
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::numeric::is_valid_number_input_value;
|
||||
use super::numeric::{is_valid_number_input_value, number_aligns_to_step};
|
||||
use moli_html_input_temporal::{
|
||||
datetime_local_input_milliseconds, datetime_local_input_value_from_milliseconds,
|
||||
is_valid_date_input_value, is_valid_month_input_value, is_valid_time_input_value,
|
||||
@@ -33,10 +33,34 @@ pub fn sanitize_input_value_for_type(input_type: InputType, value: &str) -> Stri
|
||||
sanitize_input_value_for_type_with_multiple(input_type, value, false)
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub(crate) struct InputValueSanitizationContext<'a> {
|
||||
pub multiple: bool,
|
||||
pub min: Option<&'a str>,
|
||||
pub max: Option<&'a str>,
|
||||
pub step: Option<&'a str>,
|
||||
pub value_attribute: Option<&'a str>,
|
||||
}
|
||||
|
||||
pub fn sanitize_input_value_for_type_with_multiple(
|
||||
input_type: InputType,
|
||||
value: &str,
|
||||
multiple: bool,
|
||||
) -> String {
|
||||
sanitize_input_value_for_type_with_context(
|
||||
input_type,
|
||||
value,
|
||||
InputValueSanitizationContext {
|
||||
multiple,
|
||||
..InputValueSanitizationContext::default()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn sanitize_input_value_for_type_with_context(
|
||||
input_type: InputType,
|
||||
value: &str,
|
||||
context: InputValueSanitizationContext<'_>,
|
||||
) -> String {
|
||||
match input_type {
|
||||
// Text-family — strip newlines / CR but leave whitespace runs alone.
|
||||
@@ -48,7 +72,7 @@ pub fn sanitize_input_value_for_type_with_multiple(
|
||||
let stripped = strip_input_value_line_breaks(value);
|
||||
stripped.trim_matches(is_ascii_whitespace_char).to_owned()
|
||||
}
|
||||
InputType::Email => sanitize_email_input_value(value, multiple),
|
||||
InputType::Email => sanitize_email_input_value(value, context.multiple),
|
||||
InputType::Number if !is_valid_number_input_value(value) => String::new(),
|
||||
InputType::Date if !is_valid_date_input_value(value) => String::new(),
|
||||
InputType::Time if !is_valid_time_input_value(value) => String::new(),
|
||||
@@ -57,11 +81,7 @@ pub fn sanitize_input_value_for_type_with_multiple(
|
||||
.unwrap_or_default(),
|
||||
InputType::Month if !is_valid_month_input_value(value) => String::new(),
|
||||
InputType::Week if !is_valid_week_input_value(value) => String::new(),
|
||||
// Range — sanitization: parse as float, clamp to [min, max], else
|
||||
// default to "(min+max)/2". Without min/max context here we use the
|
||||
// attribute-less default range 0..=100 (midpoint 50). Callers with
|
||||
// attribute context can override.
|
||||
InputType::Range => sanitize_range_value(value),
|
||||
InputType::Range => sanitize_range_value(value, context),
|
||||
// HTML-compatible color inputs accept CSS colors, discard alpha, and
|
||||
// expose an opaque lowercase sRGB simple color.
|
||||
InputType::Color => sanitize_color_value(value),
|
||||
@@ -112,22 +132,87 @@ fn is_ascii_whitespace_char(ch: char) -> bool {
|
||||
matches!(ch, ' ' | '\t' | '\n' | '\r' | '\x0C')
|
||||
}
|
||||
|
||||
fn sanitize_range_value(value: &str) -> String {
|
||||
// Default min=0, max=100 per spec — without attribute access here we
|
||||
// can only honour that default; element-aware callers should override
|
||||
// by computing the suggested default themselves.
|
||||
match value.trim_matches(is_ascii_whitespace_char).parse::<f64>() {
|
||||
Ok(parsed) if parsed.is_finite() => {
|
||||
// Clamp to [0, 100]; preserve integer formatting when the result
|
||||
// is an integer to match the HTML serializer for valid floats.
|
||||
let clamped = parsed.clamp(0.0, 100.0);
|
||||
if clamped == clamped.trunc() {
|
||||
(clamped as i64).to_string()
|
||||
} else {
|
||||
clamped.to_string()
|
||||
}
|
||||
}
|
||||
_ => "50".to_owned(),
|
||||
fn sanitize_range_value(value: &str, context: InputValueSanitizationContext<'_>) -> String {
|
||||
let minimum = context
|
||||
.min
|
||||
.and_then(parse_valid_range_number)
|
||||
.unwrap_or(0.0);
|
||||
let maximum = context
|
||||
.max
|
||||
.and_then(parse_valid_range_number)
|
||||
.unwrap_or(100.0)
|
||||
.max(minimum);
|
||||
let step_base = context
|
||||
.min
|
||||
.and_then(parse_valid_range_number)
|
||||
.or_else(|| context.value_attribute.and_then(parse_valid_range_number))
|
||||
.unwrap_or(0.0);
|
||||
let step = match context.step {
|
||||
Some(step) if step.eq_ignore_ascii_case("any") => None,
|
||||
Some(step) => Some(
|
||||
parse_valid_range_number(step)
|
||||
.filter(|step| *step > 0.0)
|
||||
.unwrap_or(1.0),
|
||||
),
|
||||
None => Some(1.0),
|
||||
};
|
||||
|
||||
let clamp = |value| clamp_range_value(value, minimum, maximum, step_base, step);
|
||||
let default_value = clamp(minimum / 2.0 + maximum / 2.0);
|
||||
let value = parse_valid_range_number(value).unwrap_or(default_value);
|
||||
serialize_range_number(clamp(value))
|
||||
}
|
||||
|
||||
fn parse_valid_range_number(value: &str) -> Option<f64> {
|
||||
if value.is_empty() || !is_valid_number_input_value(value) {
|
||||
return None;
|
||||
}
|
||||
value.parse::<f64>().ok().filter(|value| value.is_finite())
|
||||
}
|
||||
|
||||
fn clamp_range_value(
|
||||
value: f64,
|
||||
minimum: f64,
|
||||
maximum: f64,
|
||||
step_base: f64,
|
||||
step: Option<f64>,
|
||||
) -> f64 {
|
||||
let in_range = value.clamp(minimum, maximum);
|
||||
let Some(step) = step else {
|
||||
return in_range;
|
||||
};
|
||||
if number_aligns_to_step(in_range, step_base, step) {
|
||||
return in_range;
|
||||
}
|
||||
|
||||
let quotient = (in_range - step_base) / step;
|
||||
if !quotient.is_finite() {
|
||||
return in_range;
|
||||
}
|
||||
let lower = quotient.floor();
|
||||
let rounded = if quotient - lower < 0.5 {
|
||||
lower
|
||||
} else {
|
||||
lower + 1.0
|
||||
};
|
||||
let mut candidate = step_base + rounded * step;
|
||||
if candidate > maximum {
|
||||
candidate -= step;
|
||||
} else if candidate < minimum {
|
||||
candidate += step;
|
||||
}
|
||||
if candidate < minimum || candidate > maximum || !candidate.is_finite() {
|
||||
in_range
|
||||
} else {
|
||||
candidate
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_range_number(value: f64) -> String {
|
||||
if value == 0.0 {
|
||||
"0".to_owned()
|
||||
} else {
|
||||
value.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,7 +311,10 @@ pub fn url_value_type_mismatch(value: &str) -> bool {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{sanitize_input_value_for_type, sanitize_input_value_for_type_with_multiple};
|
||||
use super::{
|
||||
InputValueSanitizationContext, sanitize_input_value_for_type,
|
||||
sanitize_input_value_for_type_with_context, sanitize_input_value_for_type_with_multiple,
|
||||
};
|
||||
use moli_html_input_type::InputType;
|
||||
|
||||
const INITIAL: &str = " foo\rbar ";
|
||||
@@ -330,6 +418,59 @@ mod tests {
|
||||
assert_eq!(sanitize_input_value_for_type(InputType::Range, "-5"), "0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn range_uses_element_limits_and_step_alignment() {
|
||||
let sanitize = |value, min, max, step, value_attribute| {
|
||||
sanitize_input_value_for_type_with_context(
|
||||
InputType::Range,
|
||||
value,
|
||||
InputValueSanitizationContext {
|
||||
multiple: false,
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
value_attribute,
|
||||
},
|
||||
)
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
sanitize("ppp", Some("0"), Some("5"), Some("xyz"), Some("ppp")),
|
||||
"3"
|
||||
);
|
||||
assert_eq!(sanitize("7", Some("0"), Some("5"), None, Some("7")), "5");
|
||||
assert_eq!(sanitize("", Some("2"), Some("6"), None, None), "4");
|
||||
assert_eq!(sanitize("", Some("0"), Some("7"), Some("2"), None), "4");
|
||||
assert_eq!(sanitize("", Some("2"), Some("-3"), None, None), "2");
|
||||
assert_eq!(
|
||||
sanitize("6.7", Some("5"), Some("12.6"), None, Some("6.7")),
|
||||
"7"
|
||||
);
|
||||
assert_eq!(
|
||||
sanitize("6.7", Some("5.3"), Some("12"), None, Some("6.7")),
|
||||
"6.3"
|
||||
);
|
||||
assert_eq!(
|
||||
sanitize("6.7", Some("5.3"), Some("12"), Some("0.5"), Some("6.7")),
|
||||
"6.8"
|
||||
);
|
||||
assert_eq!(sanitize(" 123", None, None, None, Some(" 123")), "50");
|
||||
assert_eq!(
|
||||
sanitize("", Some("0"), Some("100"), Some("20"), Some("40")),
|
||||
"60"
|
||||
);
|
||||
assert_eq!(
|
||||
sanitize("0.6", Some("0"), Some("1"), Some("0.1"), Some("0.2")),
|
||||
"0.6"
|
||||
);
|
||||
assert_eq!(sanitize("2.1", None, None, None, None), "2");
|
||||
assert_eq!(sanitize("2.1", None, None, Some("any"), None), "2.1");
|
||||
assert_eq!(
|
||||
sanitize("-2.5", Some("-10"), Some("10"), Some("1"), None),
|
||||
"-2"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn color_parses_css_colors_and_defaults_to_black_for_invalid_input() {
|
||||
assert_eq!(
|
||||
|
||||
@@ -3,6 +3,9 @@ mod numeric;
|
||||
mod text;
|
||||
mod validity;
|
||||
|
||||
pub(crate) use input_type::{
|
||||
InputValueSanitizationContext, sanitize_input_value_for_type_with_context,
|
||||
};
|
||||
pub use input_type::{
|
||||
email_value_type_mismatch, form_control_type_supports_intrinsic_validation,
|
||||
input_type_has_value_sanitization, input_type_value_mismatch, is_valid_email_address,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::Attribute;
|
||||
use crate::forms::sanitize_input_value_for_type_with_multiple;
|
||||
use crate::forms::{InputValueSanitizationContext, sanitize_input_value_for_type_with_context};
|
||||
use crate::native::NativeNodeId;
|
||||
use indexmap::IndexSet;
|
||||
use moli_html_input_type::InputType;
|
||||
@@ -246,10 +246,16 @@ impl ElementControlState {
|
||||
|
||||
if local_name == "input" {
|
||||
let input_type = InputType::from_attribute_value(attribute("type"));
|
||||
state.input_value = Some(sanitize_input_value_for_type_with_multiple(
|
||||
state.input_value = Some(sanitize_input_value_for_type_with_context(
|
||||
input_type,
|
||||
attribute("value").unwrap_or_default(),
|
||||
attribute("multiple").is_some(),
|
||||
InputValueSanitizationContext {
|
||||
multiple: attribute("multiple").is_some(),
|
||||
min: attribute("min"),
|
||||
max: attribute("max"),
|
||||
step: attribute("step"),
|
||||
value_attribute: attribute("value"),
|
||||
},
|
||||
));
|
||||
state.checked = Some(attribute("checked").is_some());
|
||||
state.selection_start = Some(0);
|
||||
@@ -875,8 +881,7 @@ impl ElementControlState {
|
||||
namespace: &str,
|
||||
local_name: &str,
|
||||
input_type: InputType,
|
||||
input_value_attribute: Option<&str>,
|
||||
input_multiple: bool,
|
||||
input_context: InputValueSanitizationContext<'_>,
|
||||
attribute_name: &str,
|
||||
attribute_value: Option<&str>,
|
||||
) {
|
||||
@@ -889,10 +894,10 @@ impl ElementControlState {
|
||||
match (local_name, attribute_name) {
|
||||
("input", "value") => {
|
||||
if !self.input_value_dirty {
|
||||
self.input_value = Some(sanitize_input_value_for_type_with_multiple(
|
||||
self.input_value = Some(sanitize_input_value_for_type_with_context(
|
||||
input_type,
|
||||
attribute_value.unwrap_or_default(),
|
||||
input_multiple,
|
||||
input_context,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -905,10 +910,10 @@ impl ElementControlState {
|
||||
let source = if self.input_value_dirty {
|
||||
self.input_value.as_deref().unwrap_or_default()
|
||||
} else {
|
||||
input_value_attribute.unwrap_or_default()
|
||||
input_context.value_attribute.unwrap_or_default()
|
||||
};
|
||||
let value =
|
||||
sanitize_input_value_for_type_with_multiple(input_type, source, input_multiple);
|
||||
sanitize_input_value_for_type_with_context(input_type, source, input_context);
|
||||
self.input_value = Some(value);
|
||||
self.input_bad_input = false;
|
||||
|
||||
@@ -927,12 +932,21 @@ impl ElementControlState {
|
||||
let source = if self.input_value_dirty {
|
||||
self.input_value.as_deref().unwrap_or_default()
|
||||
} else {
|
||||
input_value_attribute.unwrap_or_default()
|
||||
input_context.value_attribute.unwrap_or_default()
|
||||
};
|
||||
self.input_value = Some(sanitize_input_value_for_type_with_multiple(
|
||||
self.input_value = Some(sanitize_input_value_for_type_with_context(
|
||||
input_type,
|
||||
source,
|
||||
input_multiple,
|
||||
input_context,
|
||||
));
|
||||
self.input_bad_input = false;
|
||||
}
|
||||
("input", "min" | "max" | "step") if input_type == InputType::Range => {
|
||||
let source = self.input_value.as_deref().unwrap_or_default();
|
||||
self.input_value = Some(sanitize_input_value_for_type_with_context(
|
||||
input_type,
|
||||
source,
|
||||
input_context,
|
||||
));
|
||||
self.input_bad_input = false;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,8 @@ use super::NativeDom;
|
||||
use super::node::{NativeNodeId, Node};
|
||||
use crate::custom_elements::is_valid_custom_element_name;
|
||||
use crate::forms::{
|
||||
InputType, is_valid_number_input_value, sanitize_input_value_for_type_with_multiple,
|
||||
InputType, InputValueSanitizationContext, is_valid_number_input_value,
|
||||
sanitize_input_value_for_type_with_context,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -594,13 +595,29 @@ impl Element {
|
||||
}
|
||||
|
||||
fn sanitized_input_value(&self, value: &str) -> String {
|
||||
sanitize_input_value_for_type_with_multiple(
|
||||
sanitize_input_value_for_type_with_context(
|
||||
self.input_type(),
|
||||
value,
|
||||
self.has_attribute("multiple"),
|
||||
InputValueSanitizationContext {
|
||||
multiple: self.has_attribute("multiple"),
|
||||
min: self.attribute("min"),
|
||||
max: self.attribute("max"),
|
||||
step: self.attribute("step"),
|
||||
value_attribute: self.attribute("value"),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn resanitize_input_value_after_parser_attributes(&mut self) -> bool {
|
||||
if !self.is_html_input() || self.input_value_dirty() {
|
||||
return false;
|
||||
}
|
||||
let source = self.attribute("value").unwrap_or_default().to_owned();
|
||||
let value = self.sanitized_input_value(&source);
|
||||
self.control_state_mut()
|
||||
.set_input_value_with_dirty(&value, false)
|
||||
}
|
||||
|
||||
pub fn set_input_value(&mut self, value: &str) -> bool {
|
||||
if !self.is_html_input() && !self.is_html_textarea() {
|
||||
return false;
|
||||
@@ -1186,24 +1203,34 @@ impl Element {
|
||||
) {
|
||||
let is_html_input =
|
||||
self.namespace() == "http://www.w3.org/1999/xhtml" && self.local_name() == "input";
|
||||
let input_value_attribute =
|
||||
if is_html_input && matches!(attribute_name, "type" | "multiple") {
|
||||
self.attribute("value").map(str::to_owned)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let input_value_attribute = is_html_input
|
||||
.then(|| self.attribute("value").map(str::to_owned))
|
||||
.flatten();
|
||||
let input_min = is_html_input
|
||||
.then(|| self.attribute("min").map(str::to_owned))
|
||||
.flatten();
|
||||
let input_max = is_html_input
|
||||
.then(|| self.attribute("max").map(str::to_owned))
|
||||
.flatten();
|
||||
let input_step = is_html_input
|
||||
.then(|| self.attribute("step").map(str::to_owned))
|
||||
.flatten();
|
||||
let input_type = if is_html_input && attribute_name == "type" {
|
||||
InputType::from_attribute_value(attribute_value)
|
||||
} else {
|
||||
self.input_type()
|
||||
};
|
||||
let input_multiple = is_html_input && self.has_attribute("multiple");
|
||||
self.rare_data.sync_control_state_from_attribute(
|
||||
self.namespace.as_ref(),
|
||||
self.local_name.as_ref(),
|
||||
input_type,
|
||||
input_value_attribute.as_deref(),
|
||||
input_multiple,
|
||||
InputValueSanitizationContext {
|
||||
multiple: is_html_input && self.has_attribute("multiple"),
|
||||
min: input_min.as_deref(),
|
||||
max: input_max.as_deref(),
|
||||
step: input_step.as_deref(),
|
||||
value_attribute: input_value_attribute.as_deref(),
|
||||
},
|
||||
attribute_name,
|
||||
attribute_value,
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use super::{Attribute, CustomElementState, ElementControlState};
|
||||
use crate::forms::InputType;
|
||||
use crate::forms::{InputType, InputValueSanitizationContext};
|
||||
use crate::native::NativeNodeId;
|
||||
|
||||
static EMPTY_CONTROL_STATE: LazyLock<ElementControlState> =
|
||||
@@ -215,8 +215,7 @@ impl ElementRareData {
|
||||
namespace: &str,
|
||||
local_name: &str,
|
||||
input_type: InputType,
|
||||
input_value_attribute: Option<&str>,
|
||||
input_multiple: bool,
|
||||
input_context: InputValueSanitizationContext<'_>,
|
||||
attribute_name: &str,
|
||||
attribute_value: Option<&str>,
|
||||
) {
|
||||
@@ -229,8 +228,7 @@ impl ElementRareData {
|
||||
namespace,
|
||||
local_name,
|
||||
input_type,
|
||||
input_value_attribute,
|
||||
input_multiple,
|
||||
input_context,
|
||||
attribute_name,
|
||||
attribute_value,
|
||||
);
|
||||
@@ -250,8 +248,7 @@ impl ElementRareData {
|
||||
namespace,
|
||||
local_name,
|
||||
input_type,
|
||||
input_value_attribute,
|
||||
input_multiple,
|
||||
input_context,
|
||||
attribute_name,
|
||||
attribute_value,
|
||||
);
|
||||
|
||||
@@ -366,6 +366,48 @@ fn input_type_change_sanitizes_without_dirtying_default_value() {
|
||||
assert!(input.input_value_dirty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn range_input_value_uses_live_min_max_and_step_attributes() {
|
||||
let attribute = |name: &str, value: &str| {
|
||||
Attribute::new(name.to_owned(), String::new(), None, value.to_owned())
|
||||
};
|
||||
let mut input = Element::new(
|
||||
"input".to_owned(),
|
||||
"http://www.w3.org/1999/xhtml".to_owned(),
|
||||
None,
|
||||
vec![
|
||||
attribute("type", "range"),
|
||||
attribute("min", "0"),
|
||||
attribute("max", "100"),
|
||||
attribute("step", "20"),
|
||||
attribute("value", "40"),
|
||||
],
|
||||
);
|
||||
|
||||
assert_eq!(input.input_value(), "40");
|
||||
assert!(input.set_input_value(""));
|
||||
assert_eq!(input.input_value(), "60");
|
||||
assert!(input.input_value_dirty());
|
||||
|
||||
assert!(input.set_input_value("80"));
|
||||
assert!(input.set_attribute("max".to_owned(), String::new(), None, "50".to_owned()));
|
||||
assert_eq!(input.input_value(), "40");
|
||||
assert!(input.input_value_dirty());
|
||||
|
||||
let defaulted = Element::new(
|
||||
"input".to_owned(),
|
||||
"http://www.w3.org/1999/xhtml".to_owned(),
|
||||
None,
|
||||
vec![
|
||||
attribute("type", "range"),
|
||||
attribute("min", "2"),
|
||||
attribute("max", "6"),
|
||||
],
|
||||
);
|
||||
assert_eq!(defaulted.input_value(), "4");
|
||||
assert!(!defaulted.input_value_dirty());
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn heading_reflections_parse_range_and_modal_state() {
|
||||
|
||||
@@ -216,6 +216,7 @@ impl DomHost {
|
||||
changed |= attribute_changed;
|
||||
}
|
||||
}
|
||||
changed |= element.resanitize_input_value_after_parser_attributes();
|
||||
changed |= element.mark_undefined_custom_element_candidate_from_identity();
|
||||
|
||||
if changed {
|
||||
@@ -273,6 +274,37 @@ mod tests {
|
||||
assert_eq!(host.get_attribute(element, "id").as_deref(), Some("card"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parser_input_value_sanitization_observes_the_complete_token_attribute_set() {
|
||||
let mut host = test_host();
|
||||
let input = host.create_parser_element_without_attributes(
|
||||
"input".to_owned(),
|
||||
"http://www.w3.org/1999/xhtml".to_owned(),
|
||||
None,
|
||||
);
|
||||
let attribute = |name: &str, value: &str| {
|
||||
Attribute::new(name.to_owned(), String::new(), None, value.to_owned())
|
||||
};
|
||||
|
||||
host.add_attrs_if_missing_for_parser(
|
||||
input,
|
||||
vec![
|
||||
attribute("type", "range"),
|
||||
attribute("id", "range"),
|
||||
attribute("min", "2"),
|
||||
attribute("max", "6"),
|
||||
],
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
host.node(input)
|
||||
.and_then(Node::as_element)
|
||||
.map(Element::input_value)
|
||||
.as_deref(),
|
||||
Some("4")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parser_script_prepare_state_survives_parser_attribute_attach() {
|
||||
let mut host = test_host();
|
||||
|
||||
Reference in New Issue
Block a user