mirror of
https://github.com/lexmount/moli.git
synced 2026-10-03 16:00:45 +00:00
2345 lines
94 KiB
Rust
2345 lines
94 KiB
Rust
//! Selector engine integration for Moli documents.
|
|
//!
|
|
//! This crate wraps selector parsing, validation, and Stylo-backed querying on
|
|
//! top of the extracted DOM crate.
|
|
|
|
mod cssom_selector;
|
|
mod detached_stylo;
|
|
mod error;
|
|
mod stylo;
|
|
mod validation;
|
|
|
|
pub use cssom_selector::{
|
|
CssDirection, GetComputedStylePseudoElement, first_strong_text_direction,
|
|
get_computed_style_pseudo_element,
|
|
};
|
|
pub use detached_stylo::{
|
|
DetachedStyloSelectorHost, detached_stylo_selector_matches,
|
|
detached_stylo_selector_matches_if_uses_defined_pseudo, detached_stylo_selector_query_all,
|
|
};
|
|
pub use error::{SelectorError, SelectorErrorKind};
|
|
pub use moli_dom as dom;
|
|
pub use stylo::{
|
|
MoliInvalidationResult, MoliInvalidationResultBuilder, MoliInvalidationSourceResultsSink,
|
|
MoliStyleMutationElementSnapshot, MoliStyleMutationSnapshot,
|
|
StyloDependencyInvalidationFallbackContext, StyloDocument, StyloDomHostBinding,
|
|
StyloDomStyleAdapter, StyloElement, StyloElementDependencySnapshot, StyloNode,
|
|
StyloPlannedFallbackRootInvalidationTarget,
|
|
StyloPlannedFallbackRootInvalidationTargetPartsSink, StyloPlannedSourceDependencyInvalidation,
|
|
StyloPlannedSourceDependencyInvalidationPartsSink,
|
|
StyloPlannedSourceDependencyInvalidationTarget,
|
|
StyloPlannedSourceDependencyInvalidationTargetPartsSink, StyloRetainedSourceStyleInvalidation,
|
|
StyloRetainedSourceStyleInvalidationKind, StyloRetainedSourceStyleInvalidationSink,
|
|
StyloRetainedStyleChildListInvalidationQueries,
|
|
StyloRetainedStyleChildListInvalidationQueriesSink,
|
|
StyloRetainedStyleChildListInvalidationQuery, StyloRetainedStyleInvalidationQuery,
|
|
StyloRetainedStyleSiblingTraversal, StyloRuntimeFallbackRootInput, StyloShadowRoot,
|
|
StyloSourceAffectedRootsCleanup, StyloSourceAffectedRootsCleanupSink,
|
|
StyloSourceDependencyBoundaryRoots, StyloSourceDependencyInvalidationBatchPlan,
|
|
StyloSourceDependencyInvalidationBatchPlanSink, StyloSourceDependencyInvalidationBatchSource,
|
|
StyloSourceDependencyInvalidationRequest, StyloSourceDependencyRequestRequirement,
|
|
StyloSourceDependencySummary, StyloSourceFallbackRootAvailability,
|
|
StyloSourceFallbackRootAvailabilitySummary, StyloSourceFallbackRootAvailabilitySummarySink,
|
|
StyloSourceInvalidationFallbackReason, StyloSourceInvalidationFallbackReasonPlan,
|
|
StyloSourceStyleInvalidationSourceResult, StyloSourceStyleInvalidationSourceResultKind,
|
|
StyloSourceStyleInvalidationSourceResultKindSummary,
|
|
StyloSourceStyleInvalidationSourceResultKindSummarySink,
|
|
StyloSourceStyleInvalidationSourceResultParts,
|
|
StyloSourceStyleInvalidationSourceResultPartsSink,
|
|
StyloSourceStyleInvalidationSourceResultSink,
|
|
StyloSourceStyleInvalidationTargetResultCleanupFacts,
|
|
StyloSourceStyleInvalidationTargetResultCleanupFactsPartsSink,
|
|
StyloSourceStyleInvalidationTargetResultCleanupFactsSink,
|
|
StyloSourceStyleInvalidationTargetResultDiagnosticFacts,
|
|
StyloSourceStyleInvalidationTargetResultDiagnosticFactsPartsSink,
|
|
StyloSourceStyleInvalidationTargetResultDiagnosticFactsSink,
|
|
StyloSourceStyleInvalidationTargetResultRecord, StyloStateInvalidationRoot,
|
|
StyloStyleInvalidationQuery, StyloStyleInvalidationSnapshot,
|
|
StyloStyleInvalidationSnapshotAttribute, StyloStyleSourceScope,
|
|
StyloStylesheetSourceScopeFallbackInput, dom_api_selector_uses_validity_pseudo,
|
|
is_svg_presentation_attribute_name,
|
|
stylo_attribute_change_can_skip_fallback_without_dependency,
|
|
stylo_attribute_change_can_use_retained_invalidator, stylo_element_dependency_snapshot,
|
|
stylo_fallback_roots_plan, stylo_flat_tree_heading_descendants,
|
|
stylo_focus_change_invalidation_roots, stylo_focus_state_matches_handle,
|
|
stylo_focus_within_state_matches_handle,
|
|
stylo_merge_retained_source_invalidation_fallback_kind,
|
|
stylo_merge_retained_source_invalidation_kind,
|
|
stylo_merge_source_dependency_request_requirement, stylo_removed_element_dependency_snapshots,
|
|
stylo_retained_next_element_sibling, stylo_retained_previous_element_sibling,
|
|
stylo_retained_queries_for_current_element, stylo_retained_queries_for_element_snapshot,
|
|
stylo_retained_source_invalidation_kind_can_use_fallback_payload,
|
|
stylo_retained_source_style_invalidation_from_parts,
|
|
stylo_runtime_fallback_roots_for_mutation_inputs, stylo_runtime_or_source_scope_fallback_plan,
|
|
stylo_shadow_root_host_participates_in_style_scope,
|
|
stylo_source_dependency_invalidation_batch_plan,
|
|
stylo_source_fallback_reason_for_unretained_state_change,
|
|
stylo_source_invalidation_fallback_reason_plan, stylo_source_scope_fallback_plan,
|
|
stylo_state_change_can_use_retained_invalidator, stylo_stylesheet_owner_is_in_source_scope,
|
|
stylo_stylesheet_source_scope_fallback_roots,
|
|
};
|
|
|
|
use moli_dom::native::DomHost;
|
|
use moli_dom::{NodeId, native::NativeDom};
|
|
use std::{collections::HashMap, sync::OnceLock};
|
|
|
|
// Temporary self-alias so the extracted module tree can keep compiling while
|
|
// `moli` keeps its historical `crate::selector::*` references.
|
|
mod selector {
|
|
pub(crate) use crate::SelectorError;
|
|
|
|
pub(crate) mod validation {
|
|
pub(crate) use crate::validation::*;
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct QueryEngine;
|
|
|
|
#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
|
|
pub struct StyleRuleNamespaceContext {
|
|
pub default_namespace_uri: Option<String>,
|
|
pub namespace_prefixes: Vec<(String, String)>,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
|
|
pub enum StyleRuleSelectorContext {
|
|
#[default]
|
|
TopLevel,
|
|
Nested,
|
|
Scope,
|
|
}
|
|
|
|
impl StyleRuleNamespaceContext {
|
|
fn has_default_namespace(&self) -> bool {
|
|
self.default_namespace_uri.is_some()
|
|
}
|
|
|
|
fn prefixes_matching_default_namespace(&self) -> Vec<String> {
|
|
let Some(default_namespace_uri) = self.default_namespace_uri.as_deref() else {
|
|
return Vec::new();
|
|
};
|
|
self.namespace_prefixes
|
|
.iter()
|
|
.filter(|(_, namespace_uri)| namespace_uri == default_namespace_uri)
|
|
.map(|(prefix, _)| prefix.clone())
|
|
.collect()
|
|
}
|
|
|
|
fn has_namespace_prefix(&self, prefix: &str) -> bool {
|
|
self.namespace_prefixes
|
|
.iter()
|
|
.any(|(existing, _)| existing == prefix)
|
|
}
|
|
}
|
|
|
|
impl QueryEngine {
|
|
fn adapter(&self) -> &'static stylo::StyloDomApiAdapter {
|
|
static ADAPTER: OnceLock<stylo::StyloDomApiAdapter> = OnceLock::new();
|
|
ADAPTER.get_or_init(stylo::StyloDomApiAdapter::new)
|
|
}
|
|
|
|
fn host_for_document(document: &NativeDom) -> DomHost {
|
|
DomHost::from_dom(document.clone())
|
|
}
|
|
|
|
pub fn query_selector_host(
|
|
&self,
|
|
host: &DomHost,
|
|
selector: &str,
|
|
) -> Result<Option<NodeId>, SelectorError> {
|
|
self.adapter().query_selector(host, selector, None)
|
|
}
|
|
|
|
pub fn query_selector_host_with_validity_states(
|
|
&self,
|
|
host: &DomHost,
|
|
selector: &str,
|
|
validity_states: &HashMap<NodeId, bool>,
|
|
) -> Result<Option<NodeId>, SelectorError> {
|
|
self.adapter()
|
|
.query_selector(host, selector, Some(validity_states))
|
|
}
|
|
|
|
pub fn query_selector_all_host(
|
|
&self,
|
|
host: &DomHost,
|
|
selector: &str,
|
|
) -> Result<Vec<NodeId>, SelectorError> {
|
|
self.adapter().query_selector_all(host, selector, None)
|
|
}
|
|
|
|
pub fn query_selector_all_host_with_validity_states(
|
|
&self,
|
|
host: &DomHost,
|
|
selector: &str,
|
|
validity_states: &HashMap<NodeId, bool>,
|
|
) -> Result<Vec<NodeId>, SelectorError> {
|
|
self.adapter()
|
|
.query_selector_all(host, selector, Some(validity_states))
|
|
}
|
|
|
|
pub fn query_selector_in_host(
|
|
&self,
|
|
host: &DomHost,
|
|
root: NodeId,
|
|
selector: &str,
|
|
) -> Result<Option<NodeId>, SelectorError> {
|
|
self.adapter().query_selector_in(host, root, selector, None)
|
|
}
|
|
|
|
pub fn query_selector_in_host_with_validity_states(
|
|
&self,
|
|
host: &DomHost,
|
|
root: NodeId,
|
|
selector: &str,
|
|
validity_states: &HashMap<NodeId, bool>,
|
|
) -> Result<Option<NodeId>, SelectorError> {
|
|
self.adapter()
|
|
.query_selector_in(host, root, selector, Some(validity_states))
|
|
}
|
|
|
|
pub fn query_selector_all_in_host(
|
|
&self,
|
|
host: &DomHost,
|
|
root: NodeId,
|
|
selector: &str,
|
|
) -> Result<Vec<NodeId>, SelectorError> {
|
|
self.adapter()
|
|
.query_selector_all_in(host, root, selector, None)
|
|
}
|
|
|
|
pub fn query_selector_all_in_host_with_validity_states(
|
|
&self,
|
|
host: &DomHost,
|
|
root: NodeId,
|
|
selector: &str,
|
|
validity_states: &HashMap<NodeId, bool>,
|
|
) -> Result<Vec<NodeId>, SelectorError> {
|
|
self.adapter()
|
|
.query_selector_all_in(host, root, selector, Some(validity_states))
|
|
}
|
|
|
|
pub fn matches_host(
|
|
&self,
|
|
host: &DomHost,
|
|
node_id: NodeId,
|
|
selector: &str,
|
|
) -> Result<bool, SelectorError> {
|
|
self.adapter().matches(host, node_id, selector, None)
|
|
}
|
|
|
|
pub fn matches_host_with_validity_states(
|
|
&self,
|
|
host: &DomHost,
|
|
node_id: NodeId,
|
|
selector: &str,
|
|
validity_states: &HashMap<NodeId, bool>,
|
|
) -> Result<bool, SelectorError> {
|
|
self.adapter()
|
|
.matches(host, node_id, selector, Some(validity_states))
|
|
}
|
|
|
|
pub fn matches_with_scope_host(
|
|
&self,
|
|
host: &DomHost,
|
|
node_id: NodeId,
|
|
selector: &str,
|
|
scope_root: NodeId,
|
|
) -> Result<bool, SelectorError> {
|
|
self.adapter()
|
|
.matches_with_scope(host, node_id, selector, scope_root, None)
|
|
}
|
|
|
|
pub fn closest_host(
|
|
&self,
|
|
host: &DomHost,
|
|
start: NodeId,
|
|
selector: &str,
|
|
) -> Result<Option<NodeId>, SelectorError> {
|
|
self.adapter().closest(host, start, selector, None)
|
|
}
|
|
|
|
pub fn closest_host_with_validity_states(
|
|
&self,
|
|
host: &DomHost,
|
|
start: NodeId,
|
|
selector: &str,
|
|
validity_states: &HashMap<NodeId, bool>,
|
|
) -> Result<Option<NodeId>, SelectorError> {
|
|
self.adapter()
|
|
.closest(host, start, selector, Some(validity_states))
|
|
}
|
|
|
|
pub fn query_selector(
|
|
&self,
|
|
document: &NativeDom,
|
|
selector: &str,
|
|
) -> Result<Option<NodeId>, SelectorError> {
|
|
let host = Self::host_for_document(document);
|
|
self.query_selector_host(&host, selector)
|
|
}
|
|
|
|
pub fn query_selector_all(
|
|
&self,
|
|
document: &NativeDom,
|
|
selector: &str,
|
|
) -> Result<Vec<NodeId>, SelectorError> {
|
|
let host = Self::host_for_document(document);
|
|
self.query_selector_all_host(&host, selector)
|
|
}
|
|
|
|
pub fn query_selector_in(
|
|
&self,
|
|
document: &NativeDom,
|
|
root: NodeId,
|
|
selector: &str,
|
|
) -> Result<Option<NodeId>, SelectorError> {
|
|
let host = Self::host_for_document(document);
|
|
self.query_selector_in_host(&host, root, selector)
|
|
}
|
|
|
|
pub fn query_selector_all_in(
|
|
&self,
|
|
document: &NativeDom,
|
|
root: NodeId,
|
|
selector: &str,
|
|
) -> Result<Vec<NodeId>, SelectorError> {
|
|
let host = Self::host_for_document(document);
|
|
self.query_selector_all_in_host(&host, root, selector)
|
|
}
|
|
|
|
pub fn matches(
|
|
&self,
|
|
document: &NativeDom,
|
|
node_id: NodeId,
|
|
selector: &str,
|
|
) -> Result<bool, SelectorError> {
|
|
let host = Self::host_for_document(document);
|
|
self.matches_host(&host, node_id, selector)
|
|
}
|
|
|
|
pub fn matches_with_scope(
|
|
&self,
|
|
document: &NativeDom,
|
|
node_id: NodeId,
|
|
selector: &str,
|
|
scope_root: NodeId,
|
|
) -> Result<bool, SelectorError> {
|
|
let host = Self::host_for_document(document);
|
|
self.matches_with_scope_host(&host, node_id, selector, scope_root)
|
|
}
|
|
|
|
pub fn closest(
|
|
&self,
|
|
document: &NativeDom,
|
|
start: NodeId,
|
|
selector: &str,
|
|
) -> Result<Option<NodeId>, SelectorError> {
|
|
let host = Self::host_for_document(document);
|
|
self.closest_host(&host, start, selector)
|
|
}
|
|
}
|
|
|
|
pub fn html_directionality(host: &DomHost, handle: NodeId) -> CssDirection {
|
|
stylo::html_directionality(host, handle)
|
|
}
|
|
|
|
pub fn html_auto_directionality_invalidation_root(host: &DomHost, start: NodeId) -> Option<NodeId> {
|
|
stylo::html_auto_directionality_invalidation_root(host, start)
|
|
}
|
|
|
|
pub fn validate_supports_selector_condition_argument(selector: &str) -> Result<(), SelectorError> {
|
|
stylo::validate_supports_selector_condition_argument(selector)
|
|
}
|
|
|
|
pub fn normalize_scope_selector_list(selector: &str) -> Result<String, SelectorError> {
|
|
stylo::normalize_scope_selector_list(selector)
|
|
}
|
|
|
|
pub fn normalize_scope_end_selector_list(selector: &str) -> Result<String, SelectorError> {
|
|
stylo::normalize_scope_end_selector_list(selector)
|
|
}
|
|
|
|
pub fn canonicalize_cssom_style_rule_selector_text(
|
|
selector: &str,
|
|
namespace_context: &StyleRuleNamespaceContext,
|
|
rule_context: StyleRuleSelectorContext,
|
|
) -> Result<String, SelectorError> {
|
|
if cssom_selector::selector_list_has_invalid_terminal_pseudo_element_chain(selector) {
|
|
return Err(SelectorError::syntax(
|
|
"terminal pseudo-elements cannot be chained",
|
|
));
|
|
}
|
|
match rule_context {
|
|
StyleRuleSelectorContext::Nested => {
|
|
let selector = stylo::normalize_nested_style_rule_selector_list_with_namespaces(
|
|
selector,
|
|
namespace_context,
|
|
)?;
|
|
return Ok(serialize_cssom_style_rule_selector_text(
|
|
&selector,
|
|
namespace_context,
|
|
));
|
|
}
|
|
StyleRuleSelectorContext::Scope => {
|
|
let selector = stylo::normalize_scope_style_rule_selector_list_with_namespaces(
|
|
selector,
|
|
namespace_context,
|
|
)?;
|
|
return Ok(serialize_cssom_style_rule_selector_text(
|
|
&selector,
|
|
namespace_context,
|
|
));
|
|
}
|
|
StyleRuleSelectorContext::TopLevel => {}
|
|
};
|
|
let validation = if cssom_selector::selector_list_has_namespace_separator(selector) {
|
|
if let Some(prefix) =
|
|
style_rule_selector_undeclared_namespace_prefix(selector, namespace_context)
|
|
{
|
|
return Err(SelectorError::syntax(format!(
|
|
"undeclared namespace prefix `{prefix}`"
|
|
)));
|
|
}
|
|
stylo::validate_style_rule_selector_list_with_namespaces(selector, namespace_context)
|
|
} else {
|
|
stylo::validate_style_rule_selector_list(selector)
|
|
};
|
|
if let Err(error) = validation
|
|
&& !cssom_selector::dom_api_selector_list_has_only_known_pseudo_elements(selector)
|
|
{
|
|
return Err(error);
|
|
}
|
|
Ok(serialize_cssom_style_rule_selector_text(
|
|
selector,
|
|
namespace_context,
|
|
))
|
|
}
|
|
|
|
fn style_rule_selector_undeclared_namespace_prefix(
|
|
selector: &str,
|
|
namespace_context: &StyleRuleNamespaceContext,
|
|
) -> Option<String> {
|
|
cssom_selector::selector_list_namespace_prefixes(selector)
|
|
.into_iter()
|
|
.find(|prefix| !namespace_context.has_namespace_prefix(prefix))
|
|
}
|
|
|
|
fn serialize_cssom_style_rule_selector_text(
|
|
selector: &str,
|
|
namespace_context: &StyleRuleNamespaceContext,
|
|
) -> String {
|
|
let prefixes_matching_default_namespace =
|
|
namespace_context.prefixes_matching_default_namespace();
|
|
cssom_selector::serialize_cssom_selector_text_preserving_invalid_forgiving_items(
|
|
selector,
|
|
namespace_context.has_default_namespace(),
|
|
&prefixes_matching_default_namespace,
|
|
|candidate| {
|
|
stylo::validate_supports_selector_list_with_namespaces(candidate, namespace_context)
|
|
.is_ok()
|
|
},
|
|
)
|
|
.unwrap_or_else(|| selector.to_owned())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::{
|
|
QueryEngine, StyleRuleNamespaceContext, StyleRuleSelectorContext, StyloDomStyleAdapter,
|
|
canonicalize_cssom_style_rule_selector_text, html_auto_directionality_invalidation_root,
|
|
normalize_scope_end_selector_list, normalize_scope_selector_list,
|
|
validate_supports_selector_condition_argument,
|
|
};
|
|
use crate::dom::native::{DomHost, NativeDom};
|
|
use crate::stylo::{
|
|
validate_style_rule_selector_list, validate_style_rule_selector_list_with_namespaces,
|
|
validate_supports_selector_list,
|
|
};
|
|
use dom::ElementState;
|
|
use selectors::{
|
|
matching::{MatchingContext, MatchingForInvalidation, MatchingMode, NeedsSelectorFlags},
|
|
parser::{ParseRelative, SelectorList},
|
|
};
|
|
use style::dom::{TDocument, TElement};
|
|
use style::{
|
|
context::QuirksMode,
|
|
selector_parser::SelectorParser,
|
|
stylesheets::{Namespaces, Origin, UrlExtraData},
|
|
};
|
|
|
|
#[test]
|
|
fn style_rule_selector_validation_uses_css_rule_semantics() {
|
|
assert!(validate_style_rule_selector_list("article > .card::before").is_ok());
|
|
assert!(validate_style_rule_selector_list("#a#b").is_ok());
|
|
assert!(validate_style_rule_selector_list(".one, main > .two").is_ok());
|
|
assert!(validate_style_rule_selector_list("::part(mypart):lang(en)").is_ok());
|
|
assert!(validate_style_rule_selector_list("::part(mypart):dir(ltr)").is_ok());
|
|
assert!(
|
|
validate_style_rule_selector_list("span::-webkit-something-invalid:active").is_ok()
|
|
);
|
|
assert!(
|
|
validate_style_rule_selector_list("span::-webkit-something-invalid::before").is_err()
|
|
);
|
|
assert!(validate_style_rule_selector_list("").is_err());
|
|
assert!(validate_style_rule_selector_list("div[").is_err());
|
|
assert!(validate_style_rule_selector_list(".one,").is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn supports_selector_validation_disables_forgiving_selector_lists() {
|
|
assert!(validate_supports_selector_list("::part(mypart):hover").is_ok());
|
|
assert!(validate_supports_selector_list("::part(mypart):lang(en)").is_ok());
|
|
assert!(validate_supports_selector_list("::part(mypart):dir(ltr)").is_ok());
|
|
assert!(validate_supports_selector_list("::part(mypart):is(:hover)").is_ok());
|
|
assert!(validate_supports_selector_list("::part(mypart):is(:first-child)").is_err());
|
|
assert!(validate_supports_selector_list("::part(mypart):where(:first-child)").is_err());
|
|
assert!(validate_supports_selector_list("span::-webkit-something-invalid").is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn supports_selector_condition_argument_requires_single_selector() {
|
|
assert!(validate_supports_selector_condition_argument("::part(mypart):hover").is_ok());
|
|
assert!(
|
|
validate_supports_selector_condition_argument("div:is(.primary, .secondary)").is_ok()
|
|
);
|
|
assert!(validate_supports_selector_condition_argument("").is_err());
|
|
assert!(validate_supports_selector_condition_argument("div, div").is_err());
|
|
assert!(validate_supports_selector_condition_argument("div[attr=',']").is_ok());
|
|
assert!(validate_supports_selector_condition_argument("div:is(.a, .b), span").is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn cssom_style_rule_selector_canonicalization_owns_namespace_validation() {
|
|
let namespace_context = StyleRuleNamespaceContext {
|
|
default_namespace_uri: Some("http://www.w3.org/1999/xhtml".to_owned()),
|
|
namespace_prefixes: vec![
|
|
("html".to_owned(), "http://www.w3.org/1999/xhtml".to_owned()),
|
|
("svg".to_owned(), "http://www.w3.org/2000/svg".to_owned()),
|
|
],
|
|
};
|
|
|
|
assert_eq!(
|
|
canonicalize_cssom_style_rule_selector_text(
|
|
"html|div:is(.a, .b)",
|
|
&namespace_context,
|
|
StyleRuleSelectorContext::TopLevel,
|
|
)
|
|
.unwrap(),
|
|
"div:is(.a, .b)"
|
|
);
|
|
assert!(
|
|
canonicalize_cssom_style_rule_selector_text(
|
|
"math|mi",
|
|
&namespace_context,
|
|
StyleRuleSelectorContext::TopLevel,
|
|
)
|
|
.is_err()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn cssom_style_rule_selector_canonicalization_uses_rule_context() {
|
|
assert_eq!(
|
|
canonicalize_cssom_style_rule_selector_text(
|
|
"> .child",
|
|
&StyleRuleNamespaceContext::default(),
|
|
StyleRuleSelectorContext::Nested,
|
|
)
|
|
.unwrap(),
|
|
"& > .child"
|
|
);
|
|
assert_eq!(
|
|
canonicalize_cssom_style_rule_selector_text(
|
|
"> .child",
|
|
&StyleRuleNamespaceContext::default(),
|
|
StyleRuleSelectorContext::Scope,
|
|
)
|
|
.unwrap(),
|
|
"> .child"
|
|
);
|
|
assert!(
|
|
canonicalize_cssom_style_rule_selector_text(
|
|
"> .child",
|
|
&StyleRuleNamespaceContext::default(),
|
|
StyleRuleSelectorContext::TopLevel,
|
|
)
|
|
.is_err()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn scope_selector_normalization_uses_stylesheet_selector_parser() {
|
|
assert_eq!(
|
|
normalize_scope_selector_list(".a:hover, #b, div").unwrap(),
|
|
".a:hover, #b, div"
|
|
);
|
|
assert_eq!(normalize_scope_end_selector_list("> .b").unwrap(), "> .b");
|
|
assert_eq!(normalize_scope_end_selector_list("& > &").unwrap(), "& > &");
|
|
|
|
assert!(normalize_scope_selector_list("div::before").is_err());
|
|
assert!(normalize_scope_end_selector_list(">>").is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn style_rule_selector_validation_uses_stylesheet_namespaces() {
|
|
let context = StyleRuleNamespaceContext {
|
|
default_namespace_uri: Some("http://www.w3.org/1999/xhtml".to_owned()),
|
|
namespace_prefixes: vec![("svg".to_owned(), "http://www.w3.org/2000/svg".to_owned())],
|
|
};
|
|
assert!(validate_style_rule_selector_list_with_namespaces("svg|*.card", &context).is_ok());
|
|
assert!(validate_style_rule_selector_list_with_namespaces("*|*.card", &context).is_ok());
|
|
assert!(
|
|
validate_style_rule_selector_list_with_namespaces("missing|*.card", &context).is_err()
|
|
);
|
|
assert!(validate_style_rule_selector_list_with_namespaces("svg|[", &context).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn dom_api_selector_validation_rejects_digit_starting_class_names() {
|
|
let url = url::Url::parse("https://example.test/").unwrap();
|
|
let host = DomHost::from_dom(NativeDom::new_html(url));
|
|
let engine = QueryEngine;
|
|
|
|
assert!(engine.query_selector_host(&host, ".5cm").is_err());
|
|
assert!(engine.query_selector_host(&host, ".-5cm").is_err());
|
|
assert!(engine.query_selector_host(&host, ".").is_err());
|
|
assert!(engine.query_selector_host(&host, ". div").is_err());
|
|
assert!(engine.query_selector_host(&host, "., div").is_err());
|
|
assert!(engine.query_selector_host(&host, r#".\35 cm"#).is_ok());
|
|
assert!(engine.query_selector_host(&host, r#".-\35 cm"#).is_ok());
|
|
assert!(engine.query_selector_all_host(&host, "div, .5cm").is_err());
|
|
assert!(
|
|
engine
|
|
.matches_host(&host, host.document_handle(), ".5cm")
|
|
.is_err()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn dom_api_selectors_accept_duplicate_id_compounds() {
|
|
let url = url::Url::parse("https://example.test/").unwrap();
|
|
let mut host = DomHost::from_dom(NativeDom::new_html(url));
|
|
host.reset_html_document_shell();
|
|
let body = host.document_body_handle().unwrap();
|
|
let target = host.create_element("div");
|
|
assert!(host.set_attribute(target, "id", "a"));
|
|
assert!(host.append_child(body, target));
|
|
|
|
let engine = QueryEngine;
|
|
assert_eq!(
|
|
engine.query_selector_host(&host, "div#a#a").unwrap(),
|
|
Some(target)
|
|
);
|
|
assert_eq!(engine.query_selector_host(&host, "div#a#b").unwrap(), None);
|
|
}
|
|
|
|
#[test]
|
|
fn dom_api_selectors_use_the_query_roots_owner_document_quirks_mode() {
|
|
let url = url::Url::parse("https://example.test/").unwrap();
|
|
let mut dom = NativeDom::new_html(url);
|
|
dom.node_mut(dom.document_node_id())
|
|
.and_then(|node| node.data_mut().as_document_mut())
|
|
.expect("HTML DOM should have a Document")
|
|
.set_quirks_mode(QuirksMode::Quirks);
|
|
let mut host = DomHost::from_dom(dom);
|
|
host.reset_html_document_shell();
|
|
let main_document = host.document_handle();
|
|
assert_eq!(
|
|
host.document_quirks_mode_for_handle(main_document),
|
|
Some(QuirksMode::Quirks)
|
|
);
|
|
|
|
let body = host.document_body_handle().unwrap();
|
|
let container = host.create_element("div");
|
|
let adopted_button = host.create_element("button");
|
|
let remaining_button = host.create_element("button");
|
|
assert!(host.set_attribute(adopted_button, "class", "Foo"));
|
|
assert!(host.set_attribute(remaining_button, "class", "Foo"));
|
|
assert!(host.append_child(container, adopted_button));
|
|
assert!(host.append_child(container, remaining_button));
|
|
assert!(host.append_child(body, container));
|
|
|
|
let detached_document = host.create_detached_html_document();
|
|
assert!(host.append_child(detached_document, adopted_button));
|
|
assert_eq!(
|
|
host.owner_document_handle(adopted_button),
|
|
Some(detached_document)
|
|
);
|
|
|
|
let engine = QueryEngine;
|
|
assert_eq!(
|
|
engine
|
|
.query_selector_in_host(&host, detached_document, ".Foo")
|
|
.unwrap(),
|
|
Some(adopted_button)
|
|
);
|
|
assert_eq!(
|
|
engine
|
|
.query_selector_in_host(&host, detached_document, ".foo")
|
|
.unwrap(),
|
|
None
|
|
);
|
|
assert_eq!(
|
|
engine
|
|
.query_selector_in_host(&host, container, ".foo")
|
|
.unwrap(),
|
|
Some(remaining_button)
|
|
);
|
|
assert!(!engine.matches_host(&host, adopted_button, ".foo").unwrap());
|
|
assert!(
|
|
engine
|
|
.matches_host(&host, remaining_button, ".foo")
|
|
.unwrap()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn stylo_style_adapter_exposes_dom_traits_and_document_bucketed_element_data() {
|
|
let url = url::Url::parse("https://example.test/").unwrap();
|
|
let mut host = DomHost::from_dom(NativeDom::new_html(url));
|
|
host.reset_html_document_shell();
|
|
let body = host.document_body_handle().unwrap();
|
|
let target = host.create_element("input");
|
|
assert!(host.set_attribute(target, "id", "target"));
|
|
assert!(host.set_attribute(target, "class", "card selected"));
|
|
assert!(host.set_attribute(target, "disabled", ""));
|
|
assert!(host.append_child(body, target));
|
|
let shadow_host = host.create_element("div");
|
|
assert!(host.append_child(body, shadow_host));
|
|
let shadow_root = host.attach_shadow_root(shadow_host, "open").unwrap();
|
|
let shadow_child = host.create_element("span");
|
|
assert!(host.append_child(shadow_root, shadow_child));
|
|
let detached_document = host.create_detached_html_document();
|
|
let detached_target = host.create_element("section");
|
|
assert!(host.append_child(detached_document, detached_target));
|
|
|
|
let adapter = StyloDomStyleAdapter::new();
|
|
adapter.with_bound_host(&host, |adapter| {
|
|
let document = adapter.document(&host);
|
|
assert!(document.is_html_document());
|
|
assert!(document.as_node().as_document().is_some());
|
|
|
|
let element = adapter.element(&host, target).unwrap();
|
|
assert!(element.is_html_element());
|
|
assert_eq!(element.id().map(AsRef::as_ref), Some("target"));
|
|
let mut classes = Vec::new();
|
|
element.each_class(|class| classes.push(class.as_ref().to_owned()));
|
|
assert_eq!(classes, vec!["card", "selected"]);
|
|
assert!(element.state().contains(ElementState::DISABLED));
|
|
|
|
assert!(
|
|
adapter
|
|
.element(&host, shadow_host)
|
|
.unwrap()
|
|
.shadow_root()
|
|
.is_some()
|
|
);
|
|
assert!(
|
|
adapter
|
|
.element(&host, shadow_child)
|
|
.unwrap()
|
|
.containing_shadow()
|
|
.is_some()
|
|
);
|
|
|
|
assert!(!element.has_data());
|
|
unsafe {
|
|
let _data = element.ensure_data();
|
|
}
|
|
assert!(element.has_data());
|
|
assert!(adapter.has_element_data(target));
|
|
assert_eq!(adapter.element_data_count(), 1);
|
|
assert_eq!(adapter.element_side_table_document_count_for_test(), 1);
|
|
|
|
let detached_element = adapter.element(&host, detached_target).unwrap();
|
|
assert!(!detached_element.has_data());
|
|
unsafe {
|
|
let _data = detached_element.ensure_data();
|
|
}
|
|
assert!(detached_element.has_data());
|
|
assert!(adapter.has_element_data(detached_target));
|
|
assert_eq!(adapter.element_data_count(), 2);
|
|
assert_eq!(adapter.element_side_table_document_count_for_test(), 2);
|
|
|
|
unsafe {
|
|
element.clear_data();
|
|
}
|
|
assert!(!element.has_data());
|
|
assert!(!adapter.has_element_data(target));
|
|
assert!(detached_element.has_data());
|
|
assert!(adapter.has_element_data(detached_target));
|
|
assert_eq!(adapter.element_data_count(), 1);
|
|
assert_eq!(adapter.element_side_table_document_count_for_test(), 1);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
#[should_panic(expected = "style adapter host access must use the currently bound DomHost")]
|
|
fn stylo_style_adapter_rejects_cross_host_access_in_release() {
|
|
let url = url::Url::parse("https://example.test/").unwrap();
|
|
let host = DomHost::from_dom(NativeDom::new_html(url.clone()));
|
|
let other_host = DomHost::from_dom(NativeDom::new_html(url));
|
|
let adapter = StyloDomStyleAdapter::new();
|
|
|
|
adapter.with_bound_host(&host, |binding| {
|
|
let _ = binding.document(&other_host);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn dom_api_selectors_apply_target_pseudo_class_from_document_url() {
|
|
let url = url::Url::parse("https://example.test/page.html#foo%20bar").unwrap();
|
|
let mut host = DomHost::from_dom(NativeDom::new_html(url));
|
|
host.reset_html_document_shell();
|
|
let body = host.document_body_handle().unwrap();
|
|
let target = host.create_element("div");
|
|
assert!(host.set_attribute(target, "id", "foo bar"));
|
|
assert!(host.append_child(body, target));
|
|
|
|
let engine = QueryEngine;
|
|
assert_eq!(
|
|
engine.query_selector_host(&host, ":target").unwrap(),
|
|
Some(target)
|
|
);
|
|
assert!(engine.matches_host(&host, target, ":target").unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn dom_api_selectors_match_nth_child_structural_pseudo_classes() {
|
|
let url = url::Url::parse("https://example.test/").unwrap();
|
|
let mut host = DomHost::from_dom(NativeDom::new_html(url));
|
|
host.reset_html_document_shell();
|
|
let body = host.document_body_handle().unwrap();
|
|
let first = host.create_element("div");
|
|
let second = host.create_element("div");
|
|
let target = host.create_element("div");
|
|
assert!(host.append_child(body, first));
|
|
assert!(host.append_child(body, second));
|
|
assert!(host.append_child(body, target));
|
|
|
|
let engine = QueryEngine;
|
|
assert!(
|
|
engine
|
|
.matches_host(&host, target, "div:nth-child(3)")
|
|
.unwrap()
|
|
);
|
|
assert!(
|
|
engine
|
|
.matches_host(&host, target, "div:nth-child(odd of :not(.c))")
|
|
.unwrap()
|
|
);
|
|
assert!(host.set_attribute(second, "class", "c"));
|
|
assert!(
|
|
engine
|
|
.matches_host(&host, first, "body > div:nth-child(odd of :not(.c))")
|
|
.unwrap()
|
|
);
|
|
assert!(
|
|
!engine
|
|
.matches_host(&host, target, "body > div:nth-child(odd of :not(.c))")
|
|
.unwrap()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn dom_api_selectors_match_heading_pseudo_class() {
|
|
let url = url::Url::parse("https://example.test/").unwrap();
|
|
let mut host = DomHost::from_dom(NativeDom::new_html(url));
|
|
host.reset_html_document_shell();
|
|
let body = host.document_body_handle().unwrap();
|
|
let h1 = host.create_element("h1");
|
|
let h2 = host.create_element("h2");
|
|
let h7 = host.create_element("h7");
|
|
let role_heading = host.create_element("p");
|
|
let ancestor = host.create_element("section");
|
|
let nested_h1 = host.create_element("h1");
|
|
let subject = host.create_element("div");
|
|
let offset_parent = host.create_element("section");
|
|
let offset_h1 = host.create_element("h1");
|
|
let reset_boundary = host.create_element("section");
|
|
let nested_offset = host.create_element("div");
|
|
let reset_h1 = host.create_element("h1");
|
|
let shadow_host = host.create_element("section");
|
|
let shadow_h1 = host.create_element("h1");
|
|
let slot_container = host.create_element("div");
|
|
let slot = host.create_element("slot");
|
|
let slotted_h2 = host.create_element("h2");
|
|
let modal_parent = host.create_element("div");
|
|
let modal = host.create_element("dialog");
|
|
let modal_h1 = host.create_element("h1");
|
|
let aria_h1 = host.create_element("h1");
|
|
assert!(host.set_attribute(role_heading, "role", "heading"));
|
|
assert!(host.set_attribute(role_heading, "aria-level", "1"));
|
|
assert!(host.set_attribute(ancestor, "id", "ancestor"));
|
|
assert!(host.set_attribute(subject, "id", "subject"));
|
|
assert!(host.set_attribute(offset_parent, "headingoffset", "3"));
|
|
assert!(host.set_attribute(reset_boundary, "headingoffset", "2"));
|
|
assert!(host.set_attribute(reset_boundary, "headingreset", ""));
|
|
assert!(host.set_attribute(nested_offset, "headingoffset", "2"));
|
|
assert!(host.set_attribute(shadow_host, "headingoffset", "1"));
|
|
assert!(host.set_attribute(slot_container, "headingoffset", "1"));
|
|
assert!(host.set_attribute(modal_parent, "headingoffset", "8"));
|
|
assert!(host.set_attribute(aria_h1, "headingoffset", "8"));
|
|
assert!(host.set_attribute(aria_h1, "aria-level", "3"));
|
|
assert!(host.append_child(body, h1));
|
|
assert!(host.append_child(body, h2));
|
|
assert!(host.append_child(body, h7));
|
|
assert!(host.append_child(body, role_heading));
|
|
assert!(host.append_child(body, ancestor));
|
|
assert!(host.append_child(ancestor, nested_h1));
|
|
assert!(host.append_child(body, subject));
|
|
assert!(host.append_child(body, offset_parent));
|
|
assert!(host.append_child(offset_parent, offset_h1));
|
|
assert!(host.append_child(body, reset_boundary));
|
|
assert!(host.append_child(reset_boundary, nested_offset));
|
|
assert!(host.append_child(nested_offset, reset_h1));
|
|
assert!(host.append_child(body, shadow_host));
|
|
let shadow_root = host.attach_shadow_root(shadow_host, "open").unwrap();
|
|
assert!(host.append_child(shadow_root, shadow_h1));
|
|
assert!(host.append_child(shadow_root, slot_container));
|
|
assert!(host.append_child(slot_container, slot));
|
|
assert!(host.append_child(shadow_host, slotted_h2));
|
|
assert!(host.append_child(body, modal_parent));
|
|
assert!(host.append_child(modal_parent, modal));
|
|
assert!(host.append_child(modal, modal_h1));
|
|
assert!(host.set_dialog_modal(modal, true));
|
|
assert!(host.append_child(body, aria_h1));
|
|
|
|
let engine = QueryEngine;
|
|
assert!(engine.matches_host(&host, h1, ":heading").unwrap());
|
|
assert!(engine.matches_host(&host, h1, ":heading(1)").unwrap());
|
|
assert!(!engine.matches_host(&host, h1, ":heading(2)").unwrap());
|
|
assert!(engine.matches_host(&host, h2, ":heading(1, 2)").unwrap());
|
|
assert!(!engine.matches_host(&host, h7, ":heading").unwrap());
|
|
assert!(
|
|
!engine
|
|
.matches_host(&host, role_heading, ":heading(1)")
|
|
.unwrap()
|
|
);
|
|
assert_eq!(
|
|
engine.query_selector_host(&host, ":heading(2)").unwrap(),
|
|
Some(h2)
|
|
);
|
|
assert_eq!(
|
|
engine.query_selector_host(&host, ":heading(0, 7)").unwrap(),
|
|
None
|
|
);
|
|
assert!(
|
|
engine
|
|
.matches_host(&host, subject, "#ancestor:has(:heading(1)) ~ #subject")
|
|
.unwrap()
|
|
);
|
|
assert!(
|
|
engine
|
|
.matches_host(&host, offset_h1, ":heading(4)")
|
|
.unwrap()
|
|
);
|
|
assert!(host.set_attribute(offset_parent, "headingoffset", "5"));
|
|
assert!(
|
|
engine
|
|
.matches_host(&host, offset_h1, ":heading(6)")
|
|
.unwrap()
|
|
);
|
|
assert!(engine.matches_host(&host, reset_h1, ":heading(5)").unwrap());
|
|
assert!(
|
|
engine
|
|
.matches_host(&host, shadow_h1, ":heading(2)")
|
|
.unwrap()
|
|
);
|
|
assert!(
|
|
engine
|
|
.matches_host(&host, slotted_h2, ":heading(4)")
|
|
.unwrap()
|
|
);
|
|
assert!(engine.matches_host(&host, modal_h1, ":heading(1)").unwrap());
|
|
assert!(engine.matches_host(&host, aria_h1, ":heading(9)").unwrap());
|
|
assert!(engine.query_selector_host(&host, ":heading()").is_err());
|
|
assert!(engine.query_selector_host(&host, ":heading(2n)").is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn dom_api_selectors_match_custom_state_pseudo_class() {
|
|
let url = url::Url::parse("https://example.test/").unwrap();
|
|
let mut host = DomHost::from_dom(NativeDom::new_html(url));
|
|
host.reset_html_document_shell();
|
|
let body = host.document_body_handle().unwrap();
|
|
let subject = host.create_element("section");
|
|
let target = host.create_element("x-stateful");
|
|
assert!(host.append_child(body, subject));
|
|
assert!(host.append_child(subject, target));
|
|
|
|
let engine = QueryEngine;
|
|
assert!(
|
|
!engine
|
|
.matches_host(&host, target, ":state(--active)")
|
|
.unwrap()
|
|
);
|
|
assert!(
|
|
!engine
|
|
.matches_host(&host, subject, "section:has(:state(--active))")
|
|
.unwrap()
|
|
);
|
|
|
|
assert!(host.insert_custom_state(target, "--active"));
|
|
assert!(
|
|
engine
|
|
.matches_host(&host, target, ":state(--active)")
|
|
.unwrap()
|
|
);
|
|
assert!(
|
|
engine
|
|
.matches_host(&host, subject, "section:has(:state(--active))")
|
|
.unwrap()
|
|
);
|
|
assert_eq!(
|
|
engine
|
|
.query_selector_host(&host, ":state(--active)")
|
|
.unwrap(),
|
|
Some(target)
|
|
);
|
|
|
|
assert!(host.remove_custom_state(target, "--active"));
|
|
assert!(
|
|
!engine
|
|
.matches_host(&host, target, ":state(--active)")
|
|
.unwrap()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn dom_api_selectors_match_media_pseudo_classes() {
|
|
let url = url::Url::parse("https://example.test/").unwrap();
|
|
let mut host = DomHost::from_dom(NativeDom::new_html(url));
|
|
host.reset_html_document_shell();
|
|
let body = host.document_body_handle().unwrap();
|
|
let subject = host.create_element("section");
|
|
let video = host.create_element("video");
|
|
assert!(host.append_child(body, subject));
|
|
assert!(host.append_child(subject, video));
|
|
|
|
let engine = QueryEngine;
|
|
assert!(engine.matches_host(&host, video, ":paused").unwrap());
|
|
assert!(!engine.matches_host(&host, video, ":playing").unwrap());
|
|
assert!(
|
|
engine
|
|
.matches_host(&host, subject, "section:has(:paused)")
|
|
.unwrap()
|
|
);
|
|
|
|
assert!(host.set_media_paused(video, false));
|
|
assert!(!engine.matches_host(&host, video, ":paused").unwrap());
|
|
assert!(engine.matches_host(&host, video, ":playing").unwrap());
|
|
assert!(
|
|
engine
|
|
.matches_host(&host, subject, "section:has(:playing)")
|
|
.unwrap()
|
|
);
|
|
|
|
assert!(host.set_media_muted(video, true));
|
|
assert!(engine.matches_host(&host, video, ":muted").unwrap());
|
|
assert!(host.set_media_seeking(video, true));
|
|
assert!(engine.matches_host(&host, video, ":seeking").unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn stylo_style_element_matches_nth_child_of_selector_list() {
|
|
let url = url::Url::parse("https://example.test/").unwrap();
|
|
let mut host = DomHost::from_dom(NativeDom::new_html(url.clone()));
|
|
host.reset_html_document_shell();
|
|
let body = host.document_body_handle().unwrap();
|
|
let section = host.create_element("section");
|
|
let first = host.create_element("div");
|
|
let middle = host.create_element("div");
|
|
let target = host.create_element("div");
|
|
assert!(host.set_attribute(middle, "class", "c"));
|
|
assert!(host.append_child(body, section));
|
|
assert!(host.append_child(section, first));
|
|
assert!(host.append_child(section, middle));
|
|
assert!(host.append_child(section, target));
|
|
|
|
let selector = "section > div:nth-child(odd of :not(.c))";
|
|
let namespaces = Namespaces::default();
|
|
let url_data = UrlExtraData::from(url);
|
|
let parser = SelectorParser {
|
|
stylesheet_origin: Origin::Author,
|
|
namespaces: &namespaces,
|
|
url_data: &url_data,
|
|
for_supports_rule: false,
|
|
};
|
|
let mut input = cssparser::ParserInput::new(selector);
|
|
let selectors = SelectorList::parse(
|
|
&parser,
|
|
&mut cssparser::Parser::new(&mut input),
|
|
ParseRelative::No,
|
|
)
|
|
.expect("style selector should parse");
|
|
let adapter = StyloDomStyleAdapter::new();
|
|
adapter.with_bound_host(&host, |adapter| {
|
|
let first = adapter.element(&host, first).unwrap();
|
|
let target = adapter.element(&host, target).unwrap();
|
|
let mut selector_caches = selectors::matching::SelectorCaches::default();
|
|
let mut context = MatchingContext::new(
|
|
MatchingMode::Normal,
|
|
None,
|
|
&mut selector_caches,
|
|
QuirksMode::NoQuirks,
|
|
NeedsSelectorFlags::No,
|
|
MatchingForInvalidation::No,
|
|
);
|
|
assert!(selectors::matching::matches_selector_list(
|
|
&selectors,
|
|
&first,
|
|
&mut context
|
|
));
|
|
assert!(!selectors::matching::matches_selector_list(
|
|
&selectors,
|
|
&target,
|
|
&mut context
|
|
));
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn dom_api_selectors_apply_target_pseudo_class_from_utf8_fragment() {
|
|
let url = url::Url::parse("https://example.test/page.html#%E4%BD%A0").unwrap();
|
|
let mut host = DomHost::from_dom(NativeDom::new_html(url));
|
|
host.reset_html_document_shell();
|
|
let body = host.document_body_handle().unwrap();
|
|
let target = host.create_element("div");
|
|
assert!(host.set_attribute(target, "id", "你"));
|
|
assert!(host.append_child(body, target));
|
|
|
|
let engine = QueryEngine;
|
|
assert_eq!(
|
|
engine.query_selector_host(&host, ":target").unwrap(),
|
|
Some(target)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn dom_api_selectors_apply_target_state_for_complex_selectors() {
|
|
let url = url::Url::parse("https://example.test/page.html#target").unwrap();
|
|
let mut host = DomHost::from_dom(NativeDom::new_html(url));
|
|
host.reset_html_document_shell();
|
|
let body = host.document_body_handle().unwrap();
|
|
let target = host.create_element("section");
|
|
assert!(host.set_attribute(target, "id", "target"));
|
|
assert!(host.set_attribute(target, "class", "hit"));
|
|
assert!(host.set_attribute(target, "data-x", "ok"));
|
|
let child = host.create_element("p");
|
|
assert!(host.append_child(target, child));
|
|
assert!(host.append_child(body, target));
|
|
|
|
let engine = QueryEngine;
|
|
assert_eq!(
|
|
engine
|
|
.query_selector_host(&host, "section.hit[data-x]:target")
|
|
.unwrap(),
|
|
Some(target)
|
|
);
|
|
assert_eq!(
|
|
engine.query_selector_host(&host, ":target > p").unwrap(),
|
|
Some(child)
|
|
);
|
|
assert!(!engine.matches_host(&host, target, ":not(:target)").unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn dom_api_selectors_update_target_state_when_fragment_changes() {
|
|
let url = url::Url::parse("https://example.test/page.html#first").unwrap();
|
|
let mut host = DomHost::from_dom(NativeDom::new_html(url));
|
|
host.reset_html_document_shell();
|
|
let body = host.document_body_handle().unwrap();
|
|
let first = host.create_element("div");
|
|
let second = host.create_element("div");
|
|
assert!(host.set_attribute(first, "id", "first"));
|
|
assert!(host.set_attribute(second, "id", "second"));
|
|
assert!(host.append_child(body, first));
|
|
assert!(host.append_child(body, second));
|
|
|
|
let engine = QueryEngine;
|
|
assert!(engine.matches_host(&host, first, ":target").unwrap());
|
|
assert!(!engine.matches_host(&host, second, ":target").unwrap());
|
|
|
|
host.set_document_url(url::Url::parse("https://example.test/page.html#second").unwrap());
|
|
|
|
assert!(!engine.matches_host(&host, first, ":target").unwrap());
|
|
assert!(engine.matches_host(&host, second, ":target").unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn dom_api_selectors_clear_target_state_when_target_stops_matching() {
|
|
let url = url::Url::parse("https://example.test/page.html#target").unwrap();
|
|
let mut host = DomHost::from_dom(NativeDom::new_html(url));
|
|
host.reset_html_document_shell();
|
|
let body = host.document_body_handle().unwrap();
|
|
let target = host.create_element("div");
|
|
assert!(host.set_attribute(target, "id", "target"));
|
|
assert!(host.append_child(body, target));
|
|
|
|
let engine = QueryEngine;
|
|
assert!(engine.matches_host(&host, target, ":target").unwrap());
|
|
|
|
assert!(host.set_attribute(target, "id", "other"));
|
|
assert!(!engine.matches_host(&host, target, ":target").unwrap());
|
|
|
|
assert!(host.set_attribute(target, "id", "target"));
|
|
assert!(engine.matches_host(&host, target, ":target").unwrap());
|
|
|
|
assert!(host.remove_child(body, target));
|
|
assert!(!engine.matches_host(&host, target, ":target").unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn dom_api_selectors_match_persistent_hover_state() {
|
|
let url = url::Url::parse("https://example.test/").unwrap();
|
|
let mut host = DomHost::from_dom(NativeDom::new_html(url));
|
|
host.reset_html_document_shell();
|
|
let body = host.document_body_handle().unwrap();
|
|
let menu = host.create_element("nav");
|
|
let item = host.create_element("button");
|
|
let sibling = host.create_element("button");
|
|
assert!(host.append_child(menu, item));
|
|
assert!(host.append_child(body, menu));
|
|
assert!(host.append_child(body, sibling));
|
|
|
|
let engine = QueryEngine;
|
|
assert!(!engine.matches_host(&host, item, ":hover").unwrap());
|
|
assert!(!engine.matches_host(&host, menu, ":hover").unwrap());
|
|
|
|
assert!(host.set_hovered_element_handles(vec![item, menu, body]));
|
|
assert!(engine.matches_host(&host, item, ":hover").unwrap());
|
|
assert!(engine.matches_host(&host, menu, ":hover").unwrap());
|
|
assert!(
|
|
engine
|
|
.matches_host(&host, menu, ":hover:has(> button:hover)")
|
|
.unwrap()
|
|
);
|
|
assert!(!engine.matches_host(&host, sibling, ":hover").unwrap());
|
|
|
|
assert!(host.set_hovered_element_handles(vec![sibling, body]));
|
|
assert!(!engine.matches_host(&host, item, ":hover").unwrap());
|
|
assert!(!engine.matches_host(&host, menu, ":hover").unwrap());
|
|
assert!(engine.matches_host(&host, sibling, ":hover").unwrap());
|
|
|
|
assert!(host.remove_child(body, sibling));
|
|
assert!(!engine.matches_host(&host, sibling, ":hover").unwrap());
|
|
assert!(host.append_child(body, sibling));
|
|
assert!(!engine.matches_host(&host, sibling, ":hover").unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn dom_api_selectors_clear_target_state_for_empty_fragment_and_top() {
|
|
let url = url::Url::parse("https://example.test/page.html#target").unwrap();
|
|
let mut host = DomHost::from_dom(NativeDom::new_html(url));
|
|
host.reset_html_document_shell();
|
|
let body = host.document_body_handle().unwrap();
|
|
let target = host.create_element("div");
|
|
assert!(host.set_attribute(target, "id", "target"));
|
|
assert!(host.append_child(body, target));
|
|
|
|
let engine = QueryEngine;
|
|
assert!(engine.matches_host(&host, target, ":target").unwrap());
|
|
|
|
host.set_document_url(url::Url::parse("https://example.test/page.html#").unwrap());
|
|
assert_eq!(engine.query_selector_host(&host, ":target").unwrap(), None);
|
|
assert!(!engine.matches_host(&host, target, ":target").unwrap());
|
|
|
|
host.set_document_url(url::Url::parse("https://example.test/page.html#target").unwrap());
|
|
assert!(engine.matches_host(&host, target, ":target").unwrap());
|
|
|
|
host.set_document_url(url::Url::parse("https://example.test/page.html#top").unwrap());
|
|
assert_eq!(engine.query_selector_host(&host, ":target").unwrap(), None);
|
|
assert!(!engine.matches_host(&host, target, ":target").unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn dom_api_selectors_resolve_target_raw_before_decoded_and_anchor_name() {
|
|
let url = url::Url::parse("https://example.test/page.html#foo%20bar").unwrap();
|
|
let mut host = DomHost::from_dom(NativeDom::new_html(url));
|
|
host.reset_html_document_shell();
|
|
let body = host.document_body_handle().unwrap();
|
|
let raw = host.create_element("div");
|
|
let decoded = host.create_element("div");
|
|
assert!(host.set_attribute(raw, "id", "foo%20bar"));
|
|
assert!(host.set_attribute(decoded, "id", "foo bar"));
|
|
assert!(host.append_child(body, raw));
|
|
assert!(host.append_child(body, decoded));
|
|
|
|
let engine = QueryEngine;
|
|
assert_eq!(
|
|
engine.query_selector_host(&host, ":target").unwrap(),
|
|
Some(raw)
|
|
);
|
|
|
|
let mut name_host = DomHost::from_dom(NativeDom::new_html(
|
|
url::Url::parse("https://example.test/page.html#legacy").unwrap(),
|
|
));
|
|
name_host.reset_html_document_shell();
|
|
let body = name_host.document_body_handle().unwrap();
|
|
let anchor = name_host.create_element("a");
|
|
assert!(name_host.set_attribute(anchor, "name", "legacy"));
|
|
assert!(name_host.append_child(body, anchor));
|
|
|
|
assert_eq!(
|
|
engine.query_selector_host(&name_host, ":target").unwrap(),
|
|
Some(anchor)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn dom_api_selectors_treat_known_pseudo_elements_as_empty_results() {
|
|
let url = url::Url::parse("https://example.test/").unwrap();
|
|
let mut host = DomHost::from_dom(NativeDom::new_html(url));
|
|
host.reset_html_document_shell();
|
|
let body = host.document_body_handle().unwrap();
|
|
let target = host.create_element("div");
|
|
assert!(host.set_attribute(target, "id", "target"));
|
|
assert!(host.append_child(body, target));
|
|
|
|
let engine = QueryEngine;
|
|
assert_eq!(
|
|
engine
|
|
.query_selector_all_host(&host, "#target::before")
|
|
.unwrap(),
|
|
Vec::new()
|
|
);
|
|
assert_eq!(
|
|
engine
|
|
.query_selector_host(&host, "#target:first-line")
|
|
.unwrap(),
|
|
None
|
|
);
|
|
assert_eq!(
|
|
engine.query_selector_host(&host, "::slotted(foo").unwrap(),
|
|
None
|
|
);
|
|
assert_eq!(
|
|
engine
|
|
.query_selector_host(&host, "::part(label):hover")
|
|
.unwrap(),
|
|
None
|
|
);
|
|
assert_eq!(
|
|
engine
|
|
.query_selector_host(&host, "::part(label):lang(en)")
|
|
.unwrap(),
|
|
None
|
|
);
|
|
for selector in [
|
|
"::slotted(*)::checkmark",
|
|
"::slotted(*)::picker-icon",
|
|
"::slotted(*)::picker(select)",
|
|
] {
|
|
assert_eq!(
|
|
engine.query_selector_host(&host, selector).unwrap(),
|
|
None,
|
|
"valid pseudo-element selector `{selector}` should return no element"
|
|
);
|
|
}
|
|
for selector in [
|
|
"::slotted()",
|
|
"::slotted(",
|
|
"::slotted(0)",
|
|
":slotted(foo)",
|
|
"::slotted(foo) + ::slotted(bar)",
|
|
"::slotted(foo):hover",
|
|
"::slotted(foo):focus",
|
|
"::slotted(foo):lang(en)",
|
|
"::slotted(foo):dir(ltr)",
|
|
":part()",
|
|
"::part(",
|
|
":part(0)",
|
|
":part('foo')",
|
|
":part([foo])",
|
|
"::part(foo) + ::part(bar)",
|
|
] {
|
|
assert!(
|
|
engine.query_selector_host(&host, selector).is_err(),
|
|
"invalid pseudo-element selector `{selector}` should be rejected"
|
|
);
|
|
}
|
|
assert!(
|
|
engine
|
|
.query_selector_host(&host, "::part(label):first-child")
|
|
.is_err()
|
|
);
|
|
assert!(
|
|
engine
|
|
.query_selector_host(&host, "#target::example")
|
|
.is_err()
|
|
);
|
|
assert!(
|
|
engine
|
|
.query_selector_host(&host, "#target::before[")
|
|
.is_err()
|
|
);
|
|
assert!(
|
|
engine
|
|
.query_selector_host(&host, "#target::before, [")
|
|
.is_err()
|
|
);
|
|
assert!(
|
|
engine
|
|
.query_selector_host(&host, "invalid# ::before")
|
|
.is_err()
|
|
);
|
|
assert_eq!(
|
|
engine
|
|
.query_selector_host(&host, "span::-webkit-something-invalid")
|
|
.unwrap(),
|
|
None
|
|
);
|
|
assert_eq!(
|
|
engine
|
|
.query_selector_host(&host, "span::-webkit-something-invalid:active")
|
|
.unwrap(),
|
|
None
|
|
);
|
|
assert!(
|
|
engine
|
|
.query_selector_host(&host, "span::-webkit-something-invalid::before")
|
|
.is_err()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn dom_api_selectors_recover_trailing_unclosed_attribute_selector() {
|
|
let url = url::Url::parse("https://example.test/").unwrap();
|
|
let mut host = DomHost::from_dom(NativeDom::new_html(url));
|
|
host.reset_html_document_shell();
|
|
let body = host.document_body_handle().unwrap();
|
|
let target = host.create_element("div");
|
|
assert!(host.set_attribute(target, "align", "center"));
|
|
assert!(host.append_child(body, target));
|
|
|
|
let engine = QueryEngine;
|
|
assert_eq!(
|
|
engine
|
|
.query_selector_host(&host, r#"[align="center""#)
|
|
.unwrap(),
|
|
Some(target)
|
|
);
|
|
assert!(
|
|
engine
|
|
.matches_host(&host, target, r#"[align="center""#)
|
|
.unwrap()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn dom_api_selectors_empty_substring_attribute_operators_match_nothing() {
|
|
let url = url::Url::parse("https://example.test/").unwrap();
|
|
let mut host = DomHost::from_dom(NativeDom::new_html(url));
|
|
host.reset_html_document_shell();
|
|
let body = host.document_body_handle().unwrap();
|
|
let target = host.create_element("div");
|
|
assert!(host.set_attribute(target, "class", "hit"));
|
|
assert!(host.set_attribute(target, "data-x", "value"));
|
|
assert!(host.append_child(body, target));
|
|
|
|
let engine = QueryEngine;
|
|
assert_eq!(
|
|
engine
|
|
.query_selector_host(&host, r#"[data-x^=""]"#)
|
|
.unwrap(),
|
|
None
|
|
);
|
|
assert_eq!(
|
|
engine
|
|
.query_selector_host(&host, r#"[data-x$=""]"#)
|
|
.unwrap(),
|
|
None
|
|
);
|
|
assert_eq!(
|
|
engine
|
|
.query_selector_host(&host, r#"[data-x*=""]"#)
|
|
.unwrap(),
|
|
None
|
|
);
|
|
assert!(
|
|
!engine
|
|
.matches_host(&host, target, r#"[class^=""]"#)
|
|
.unwrap()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn dom_api_selectors_wildcard_namespace_attribute_scans_all_matching_local_names() {
|
|
let url = url::Url::parse("https://example.test/").unwrap();
|
|
let mut host = DomHost::from_dom(NativeDom::new_html(url));
|
|
host.reset_html_document_shell();
|
|
let body = host.document_body_handle().unwrap();
|
|
let target = host.create_element("div");
|
|
assert!(host.set_attribute(target, "foo", "x"));
|
|
assert!(host.set_attribute_ns(target, Some("a"), Some("a"), "foo", "x"));
|
|
assert!(host.set_attribute_ns(target, Some("b"), Some("b"), "foo", "BAR"));
|
|
assert!(host.append_child(body, target));
|
|
|
|
let engine = QueryEngine;
|
|
assert!(
|
|
engine
|
|
.matches_host(&host, target, "[*|foo='bar' i]")
|
|
.unwrap()
|
|
);
|
|
assert_eq!(
|
|
engine
|
|
.query_selector_host(&host, "[*|foo='bar' i]")
|
|
.unwrap(),
|
|
Some(target)
|
|
);
|
|
assert!(
|
|
!engine
|
|
.matches_host(&host, target, "[|foo='bar' i]")
|
|
.unwrap()
|
|
);
|
|
assert!(!engine.matches_host(&host, target, "[foo='bar' i]").unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn dom_api_selectors_contenteditable_false_blocks_read_write() {
|
|
let url = url::Url::parse("https://example.test/").unwrap();
|
|
let mut host = DomHost::from_dom(NativeDom::new_html(url));
|
|
host.reset_html_document_shell();
|
|
let body = host.document_body_handle().unwrap();
|
|
let editable = host.create_element("div");
|
|
assert!(host.set_attribute(editable, "contenteditable", "true"));
|
|
let inherited = host.create_element("p");
|
|
assert!(host.append_child(editable, inherited));
|
|
let blocked = host.create_element("section");
|
|
assert!(host.set_attribute(blocked, "contenteditable", "false"));
|
|
assert!(host.append_child(editable, blocked));
|
|
let blocked_child = host.create_element("span");
|
|
assert!(host.append_child(blocked, blocked_child));
|
|
let invalid_inherits = host.create_element("em");
|
|
assert!(host.set_attribute(invalid_inherits, "contenteditable", "invalid"));
|
|
assert!(host.append_child(editable, invalid_inherits));
|
|
let plaintext = host.create_element("article");
|
|
assert!(host.set_attribute(plaintext, "contenteditable", "plaintext-only"));
|
|
assert!(host.append_child(body, editable));
|
|
assert!(host.append_child(body, plaintext));
|
|
|
|
let engine = QueryEngine;
|
|
assert!(engine.matches_host(&host, editable, ":read-write").unwrap());
|
|
assert!(
|
|
engine
|
|
.matches_host(&host, inherited, ":read-write")
|
|
.unwrap()
|
|
);
|
|
assert!(
|
|
engine
|
|
.matches_host(&host, invalid_inherits, ":read-write")
|
|
.unwrap()
|
|
);
|
|
assert!(
|
|
engine
|
|
.matches_host(&host, plaintext, ":read-write")
|
|
.unwrap()
|
|
);
|
|
assert!(!engine.matches_host(&host, blocked, ":read-write").unwrap());
|
|
assert!(
|
|
!engine
|
|
.matches_host(&host, blocked_child, ":read-write")
|
|
.unwrap()
|
|
);
|
|
assert!(engine.matches_host(&host, blocked, ":read-only").unwrap());
|
|
assert!(
|
|
engine
|
|
.matches_host(&host, blocked_child, ":read-only")
|
|
.unwrap()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn dom_api_selectors_design_mode_marks_connected_elements_read_write() {
|
|
let url = url::Url::parse("https://example.test/").unwrap();
|
|
let mut host = DomHost::from_dom(NativeDom::new_html(url));
|
|
host.reset_html_document_shell();
|
|
let body = host.document_body_handle().unwrap();
|
|
let paragraph = host.create_element("p");
|
|
let blocked = host.create_element("div");
|
|
assert!(host.set_attribute(blocked, "contenteditable", "false"));
|
|
let detached = host.create_element("section");
|
|
assert!(host.append_child(body, paragraph));
|
|
assert!(host.append_child(body, blocked));
|
|
assert!(host.set_document_design_mode_enabled_for_handle(host.document_handle(), true));
|
|
|
|
let engine = QueryEngine;
|
|
assert!(
|
|
engine
|
|
.matches_host(&host, paragraph, ":read-write")
|
|
.unwrap()
|
|
);
|
|
assert!(!engine.matches_host(&host, paragraph, ":read-only").unwrap());
|
|
assert!(!engine.matches_host(&host, blocked, ":read-write").unwrap());
|
|
assert!(engine.matches_host(&host, blocked, ":read-only").unwrap());
|
|
assert!(!engine.matches_host(&host, detached, ":read-write").unwrap());
|
|
assert!(engine.matches_host(&host, detached, ":read-only").unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn dom_api_selectors_lang_uses_nearest_language_attribute() {
|
|
let url = url::Url::parse("https://example.test/").unwrap();
|
|
let mut host = DomHost::from_dom(NativeDom::new_html(url));
|
|
host.reset_html_document_shell();
|
|
let body = host.document_body_handle().unwrap();
|
|
assert!(host.set_attribute(body, "lang", "en"));
|
|
let paragraph = host.create_element("p");
|
|
assert!(host.set_attribute(paragraph, "lang", "de"));
|
|
let child = host.create_element("span");
|
|
assert!(host.append_child(paragraph, child));
|
|
assert!(host.append_child(body, paragraph));
|
|
|
|
let engine = QueryEngine;
|
|
assert!(engine.matches_host(&host, paragraph, ":lang(de)").unwrap());
|
|
assert!(!engine.matches_host(&host, paragraph, ":lang(en)").unwrap());
|
|
assert!(engine.matches_host(&host, child, ":lang(de)").unwrap());
|
|
assert!(!engine.matches_host(&host, child, ":lang(en)").unwrap());
|
|
assert!(engine.matches_host(&host, body, ":lang(en)").unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn dom_api_selectors_lang_uses_stylo_extended_filtering() {
|
|
let url = url::Url::parse("https://example.test/").unwrap();
|
|
let mut host = DomHost::from_dom(NativeDom::new_html(url));
|
|
host.reset_html_document_shell();
|
|
let body = host.document_body_handle().unwrap();
|
|
let es_mx = host.create_element("p");
|
|
let es = host.create_element("p");
|
|
let en_gb_scouse = host.create_element("p");
|
|
let singleton = host.create_element("p");
|
|
let singleton_skipped = host.create_element("p");
|
|
assert!(host.set_attribute(es_mx, "lang", "es-MX"));
|
|
assert!(host.set_attribute(es, "lang", "es"));
|
|
assert!(host.set_attribute(en_gb_scouse, "lang", "en-GB-scouse"));
|
|
assert!(host.set_attribute(singleton, "lang", "fr-x-standard"));
|
|
assert!(host.set_attribute(singleton_skipped, "lang", "fr-x-standard"));
|
|
assert!(host.append_child(body, es_mx));
|
|
assert!(host.append_child(body, es));
|
|
assert!(host.append_child(body, en_gb_scouse));
|
|
assert!(host.append_child(body, singleton));
|
|
assert!(host.append_child(body, singleton_skipped));
|
|
|
|
let engine = QueryEngine;
|
|
assert!(engine.matches_host(&host, es_mx, ":lang(es)").unwrap());
|
|
assert!(
|
|
!engine
|
|
.matches_host(&host, es_mx, ":lang(es-MX-US)")
|
|
.unwrap()
|
|
);
|
|
assert!(!engine.matches_host(&host, es, ":lang(es-MX)").unwrap());
|
|
assert!(
|
|
engine
|
|
.matches_host(&host, en_gb_scouse, ":lang(en-GB)")
|
|
.unwrap()
|
|
);
|
|
assert!(
|
|
engine
|
|
.matches_host(&host, singleton, ":lang(fr-x)")
|
|
.unwrap()
|
|
);
|
|
assert!(
|
|
!engine
|
|
.matches_host(&host, singleton_skipped, ":lang(fr-standard)")
|
|
.unwrap()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn dom_api_selectors_dir_auto_uses_first_strong_text() {
|
|
let url = url::Url::parse("https://example.test/").unwrap();
|
|
let mut host = DomHost::from_dom(NativeDom::new_html(url));
|
|
host.reset_html_document_shell();
|
|
let body = host.document_body_handle().unwrap();
|
|
let bdi = host.create_element("bdi");
|
|
assert!(host.append_child(body, bdi));
|
|
|
|
let engine = QueryEngine;
|
|
assert!(engine.matches_host(&host, bdi, ":dir(ltr)").unwrap());
|
|
assert!(!engine.matches_host(&host, bdi, ":dir(rtl)").unwrap());
|
|
|
|
assert!(host.set_text_content(bdi, "\u{05ea}"));
|
|
assert!(!engine.matches_host(&host, bdi, ":dir(ltr)").unwrap());
|
|
assert!(engine.matches_host(&host, bdi, ":dir(rtl)").unwrap());
|
|
|
|
let parent = host.create_element("div");
|
|
let isolated = host.create_element("div");
|
|
let fallback = host.create_element("div");
|
|
assert!(host.set_attribute(parent, "dir", "auto"));
|
|
assert!(host.set_attribute(isolated, "dir", "rtl"));
|
|
assert!(host.set_text_content(isolated, "\u{05ea}"));
|
|
assert!(host.set_text_content(fallback, "a"));
|
|
assert!(host.append_child(body, parent));
|
|
assert!(host.append_child(parent, isolated));
|
|
assert!(host.append_child(parent, fallback));
|
|
|
|
assert!(engine.matches_host(&host, parent, ":dir(ltr)").unwrap());
|
|
assert!(!engine.matches_host(&host, parent, ":dir(rtl)").unwrap());
|
|
|
|
assert!(host.set_attribute(isolated, "dir", ""));
|
|
assert!(!engine.matches_host(&host, parent, ":dir(ltr)").unwrap());
|
|
assert!(engine.matches_host(&host, parent, ":dir(rtl)").unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn dir_auto_mutation_root_stops_at_contained_text_boundaries() {
|
|
let url = url::Url::parse("https://example.test/").unwrap();
|
|
let mut host = DomHost::from_dom(NativeDom::new_html(url));
|
|
host.reset_html_document_shell();
|
|
let body = host.document_body_handle().unwrap();
|
|
|
|
let auto = host.create_element("div");
|
|
let transparent = host.create_element("span");
|
|
let explicit = host.create_element("section");
|
|
let explicit_child = host.create_element("span");
|
|
let bdi = host.create_element("bdi");
|
|
let script = host.create_element("script");
|
|
assert!(host.set_attribute(auto, "dir", "auto"));
|
|
assert!(host.set_attribute(explicit, "dir", "rtl"));
|
|
assert!(host.append_child(body, auto));
|
|
assert!(host.append_child(auto, transparent));
|
|
assert!(host.append_child(auto, explicit));
|
|
assert!(host.append_child(explicit, explicit_child));
|
|
assert!(host.append_child(auto, bdi));
|
|
assert!(host.append_child(auto, script));
|
|
|
|
assert_eq!(
|
|
html_auto_directionality_invalidation_root(&host, transparent),
|
|
Some(auto)
|
|
);
|
|
assert_eq!(
|
|
html_auto_directionality_invalidation_root(&host, explicit_child),
|
|
None
|
|
);
|
|
assert_eq!(
|
|
html_auto_directionality_invalidation_root(&host, bdi),
|
|
Some(bdi)
|
|
);
|
|
assert_eq!(
|
|
html_auto_directionality_invalidation_root(&host, script),
|
|
None
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn dom_api_selectors_dir_auto_tracks_clean_and_dirty_textarea_values() {
|
|
let url = url::Url::parse("https://example.test/").unwrap();
|
|
let mut host = DomHost::from_dom(NativeDom::new_html(url));
|
|
host.reset_html_document_shell();
|
|
let body = host.document_body_handle().unwrap();
|
|
let engine = QueryEngine;
|
|
let textarea = host.create_element("textarea");
|
|
assert!(host.set_attribute(textarea, "dir", "auto"));
|
|
let text = host.create_text_node("\u{05ea}");
|
|
assert!(host.append_child(textarea, text));
|
|
assert!(host.append_child(body, textarea));
|
|
assert!(!engine.matches_host(&host, textarea, ":dir(ltr)").unwrap());
|
|
assert!(engine.matches_host(&host, textarea, ":dir(rtl)").unwrap());
|
|
|
|
assert!(host.set_text_content(textarea, "A"));
|
|
assert!(engine.matches_host(&host, textarea, ":dir(ltr)").unwrap());
|
|
assert!(!engine.matches_host(&host, textarea, ":dir(rtl)").unwrap());
|
|
|
|
assert!(host.set_input_value(textarea, "\u{05ea}"));
|
|
assert!(host.set_text_content(textarea, "A"));
|
|
assert!(!engine.matches_host(&host, textarea, ":dir(ltr)").unwrap());
|
|
assert!(engine.matches_host(&host, textarea, ":dir(rtl)").unwrap());
|
|
|
|
assert!(host.set_input_value(textarea, "A"));
|
|
assert!(host.set_text_content(textarea, "\u{05ea}"));
|
|
assert!(engine.matches_host(&host, textarea, ":dir(ltr)").unwrap());
|
|
assert!(!engine.matches_host(&host, textarea, ":dir(rtl)").unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn dom_api_selectors_dir_on_input_uses_html_directionality_rules() {
|
|
let url = url::Url::parse("https://example.test/").unwrap();
|
|
let mut host = DomHost::from_dom(NativeDom::new_html(url));
|
|
host.reset_html_document_shell();
|
|
let body = host.document_body_handle().unwrap();
|
|
let engine = QueryEngine;
|
|
|
|
let rtl_parent = host.create_element("div");
|
|
assert!(host.set_attribute(rtl_parent, "dir", "rtl"));
|
|
assert!(host.append_child(body, rtl_parent));
|
|
|
|
let tel = host.create_element("input");
|
|
assert!(host.set_attribute(tel, "type", "tel"));
|
|
assert!(host.append_child(rtl_parent, tel));
|
|
assert!(engine.matches_host(&host, tel, ":dir(ltr)").unwrap());
|
|
assert!(!engine.matches_host(&host, tel, ":dir(rtl)").unwrap());
|
|
|
|
assert!(host.set_attribute(tel, "dir", "foo"));
|
|
assert!(engine.matches_host(&host, tel, ":dir(ltr)").unwrap());
|
|
assert!(!engine.matches_host(&host, tel, ":dir(rtl)").unwrap());
|
|
|
|
assert!(host.set_attribute(tel, "dir", "auto"));
|
|
assert!(host.set_input_value(tel, "\u{05ea}"));
|
|
assert!(!engine.matches_host(&host, tel, ":dir(ltr)").unwrap());
|
|
assert!(engine.matches_host(&host, tel, ":dir(rtl)").unwrap());
|
|
|
|
assert!(host.remove_attribute(tel, "dir"));
|
|
assert!(host.set_attribute(tel, "type", "text"));
|
|
assert!(!engine.matches_host(&host, tel, ":dir(ltr)").unwrap());
|
|
assert!(engine.matches_host(&host, tel, ":dir(rtl)").unwrap());
|
|
|
|
let auto = host.create_element("input");
|
|
assert!(host.set_attribute(auto, "dir", "auto"));
|
|
assert!(host.set_attribute(auto, "type", "text"));
|
|
assert!(host.set_input_value(auto, "\u{05ea}"));
|
|
assert!(host.append_child(body, auto));
|
|
assert!(!engine.matches_host(&host, auto, ":dir(ltr)").unwrap());
|
|
assert!(engine.matches_host(&host, auto, ":dir(rtl)").unwrap());
|
|
|
|
assert!(host.set_attribute(auto, "type", "radio"));
|
|
assert!(engine.matches_host(&host, auto, ":dir(ltr)").unwrap());
|
|
assert!(!engine.matches_host(&host, auto, ":dir(rtl)").unwrap());
|
|
|
|
assert!(host.set_attribute(auto, "type", "hidden"));
|
|
assert!(!engine.matches_host(&host, auto, ":dir(ltr)").unwrap());
|
|
assert!(engine.matches_host(&host, auto, ":dir(rtl)").unwrap());
|
|
|
|
let textarea = host.create_element("textarea");
|
|
assert!(host.set_attribute(textarea, "dir", "auto"));
|
|
let textarea_text = host.create_text_node("\u{05ea}");
|
|
assert!(host.append_child(textarea, textarea_text));
|
|
assert!(host.append_child(body, textarea));
|
|
assert!(!engine.matches_host(&host, textarea, ":dir(ltr)").unwrap());
|
|
assert!(engine.matches_host(&host, textarea, ":dir(rtl)").unwrap());
|
|
|
|
assert!(host.set_text_content(textarea, "A"));
|
|
assert!(engine.matches_host(&host, textarea, ":dir(ltr)").unwrap());
|
|
assert!(!engine.matches_host(&host, textarea, ":dir(rtl)").unwrap());
|
|
|
|
assert!(host.set_input_value(textarea, "\u{05ea}"));
|
|
assert!(host.set_text_content(textarea, "A"));
|
|
assert!(!engine.matches_host(&host, textarea, ":dir(ltr)").unwrap());
|
|
assert!(engine.matches_host(&host, textarea, ":dir(rtl)").unwrap());
|
|
|
|
assert!(host.set_input_value(textarea, "A"));
|
|
assert!(engine.matches_host(&host, textarea, ":dir(ltr)").unwrap());
|
|
assert!(!engine.matches_host(&host, textarea, ":dir(rtl)").unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn dom_api_selectors_dir_auto_resolves_slot_directionality_boundaries() {
|
|
let url = url::Url::parse("https://example.test/").unwrap();
|
|
let mut host = DomHost::from_dom(NativeDom::new_html(url));
|
|
host.reset_html_document_shell();
|
|
let body = host.document_body_handle().unwrap();
|
|
let engine = QueryEngine;
|
|
|
|
let assigned_host = host.create_element("div");
|
|
assert!(host.append_child(body, assigned_host));
|
|
let assigned_text = host.create_text_node("\u{0627}\u{062e}\u{062a}\u{0628}\u{0631}");
|
|
assert!(host.append_child(assigned_host, assigned_text));
|
|
let assigned_shadow = host.attach_shadow_root(assigned_host, "open").unwrap();
|
|
let auto_slot = host.create_element("slot");
|
|
assert!(host.set_attribute(auto_slot, "dir", "auto"));
|
|
assert!(host.append_child(assigned_shadow, auto_slot));
|
|
|
|
assert!(engine.matches_host(&host, auto_slot, ":dir(rtl)").unwrap());
|
|
assert!(!engine.matches_host(&host, auto_slot, ":dir(ltr)").unwrap());
|
|
|
|
let inherited_host = host.create_element("div");
|
|
assert!(host.set_attribute(inherited_host, "dir", "rtl"));
|
|
assert!(host.append_child(body, inherited_host));
|
|
let inherited_shadow = host.attach_shadow_root(inherited_host, "open").unwrap();
|
|
let auto_container = host.create_element("div");
|
|
assert!(host.set_attribute(auto_container, "dir", "auto"));
|
|
let inherited_slot = host.create_element("slot");
|
|
let later_ltr_text = host.create_text_node("A");
|
|
assert!(host.append_child(auto_container, inherited_slot));
|
|
assert!(host.append_child(auto_container, later_ltr_text));
|
|
assert!(host.append_child(inherited_shadow, auto_container));
|
|
|
|
assert!(
|
|
engine
|
|
.matches_host(&host, auto_container, ":dir(rtl)")
|
|
.unwrap()
|
|
);
|
|
assert!(
|
|
!engine
|
|
.matches_host(&host, auto_container, ":dir(ltr)")
|
|
.unwrap()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn dom_api_selectors_invalid_form_walks_deep_dom_iteratively() {
|
|
let url = url::Url::parse("https://example.test/").unwrap();
|
|
let mut host = DomHost::from_dom(NativeDom::new_html(url));
|
|
host.reset_html_document_shell();
|
|
let body = host.document_body_handle().unwrap();
|
|
let form = host.create_element("form");
|
|
assert!(host.append_child(body, form));
|
|
|
|
let mut parent = form;
|
|
for _ in 0..4096 {
|
|
let child = host.create_element("div");
|
|
assert!(host.append_child(parent, child));
|
|
parent = child;
|
|
}
|
|
|
|
let input = host.create_element("input");
|
|
assert!(host.set_attribute(input, "required", ""));
|
|
assert!(host.append_child(parent, input));
|
|
|
|
let engine = QueryEngine;
|
|
assert!(engine.matches_host(&host, input, ":invalid").unwrap());
|
|
assert!(engine.matches_host(&host, form, ":invalid").unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn dom_api_selectors_invalid_textarea_uses_required_branch() {
|
|
let url = url::Url::parse("https://example.test/").unwrap();
|
|
let mut host = DomHost::from_dom(NativeDom::new_html(url));
|
|
host.reset_html_document_shell();
|
|
let body = host.document_body_handle().unwrap();
|
|
let textarea = host.create_element("textarea");
|
|
assert!(host.set_attribute(textarea, "required", ""));
|
|
assert!(host.append_child(body, textarea));
|
|
|
|
let engine = QueryEngine;
|
|
assert!(engine.matches_host(&host, textarea, ":invalid").unwrap());
|
|
assert!(!engine.matches_host(&host, textarea, ":valid").unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn dom_api_selectors_optional_is_independent_of_required_support() {
|
|
let url = url::Url::parse("https://example.test/").unwrap();
|
|
let mut host = DomHost::from_dom(NativeDom::new_html(url));
|
|
host.reset_html_document_shell();
|
|
let body = host.document_body_handle().unwrap();
|
|
let engine = QueryEngine;
|
|
|
|
let input = host.create_element("input");
|
|
assert!(host.set_attribute(input, "required", ""));
|
|
assert!(host.append_child(body, input));
|
|
assert!(engine.matches_host(&host, input, ":required").unwrap());
|
|
assert!(!engine.matches_host(&host, input, ":optional").unwrap());
|
|
|
|
assert!(host.set_attribute(input, "type", "hidden"));
|
|
assert!(!engine.matches_host(&host, input, ":required").unwrap());
|
|
assert!(engine.matches_host(&host, input, ":optional").unwrap());
|
|
|
|
assert!(host.set_attribute(input, "type", "button"));
|
|
assert!(!engine.matches_host(&host, input, ":required").unwrap());
|
|
assert!(engine.matches_host(&host, input, ":optional").unwrap());
|
|
|
|
assert!(host.set_attribute(input, "type", "text"));
|
|
assert!(engine.matches_host(&host, input, ":required").unwrap());
|
|
assert!(!engine.matches_host(&host, input, ":optional").unwrap());
|
|
assert!(host.remove_attribute(input, "required"));
|
|
assert!(!engine.matches_host(&host, input, ":required").unwrap());
|
|
assert!(engine.matches_host(&host, input, ":optional").unwrap());
|
|
|
|
let button = host.create_element("button");
|
|
assert!(host.set_attribute(button, "required", ""));
|
|
assert!(host.append_child(body, button));
|
|
assert!(!engine.matches_host(&host, button, ":required").unwrap());
|
|
assert!(engine.matches_host(&host, button, ":optional").unwrap());
|
|
|
|
for local_name in ["select", "textarea"] {
|
|
let control = host.create_element(local_name);
|
|
assert!(host.set_attribute(control, "required", ""));
|
|
assert!(host.append_child(body, control));
|
|
assert!(engine.matches_host(&host, control, ":required").unwrap());
|
|
assert!(!engine.matches_host(&host, control, ":optional").unwrap());
|
|
assert!(host.remove_attribute(control, "required"));
|
|
assert!(!engine.matches_host(&host, control, ":required").unwrap());
|
|
assert!(engine.matches_host(&host, control, ":optional").unwrap());
|
|
}
|
|
|
|
let div = host.create_element("div");
|
|
assert!(host.set_attribute(div, "required", ""));
|
|
assert!(host.append_child(body, div));
|
|
assert!(!engine.matches_host(&host, div, ":required").unwrap());
|
|
assert!(!engine.matches_host(&host, div, ":optional").unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn dom_api_selectors_readonly_input_is_barred_from_range_pseudos() {
|
|
let url = url::Url::parse("https://example.test/").unwrap();
|
|
let mut host = DomHost::from_dom(NativeDom::new_html(url));
|
|
host.reset_html_document_shell();
|
|
let body = host.document_body_handle().unwrap();
|
|
let in_range = host.create_element("input");
|
|
assert!(host.set_attribute(in_range, "type", "number"));
|
|
assert!(host.set_attribute(in_range, "min", "1"));
|
|
assert!(host.set_attribute(in_range, "max", "10"));
|
|
assert!(host.set_attribute(in_range, "value", "5"));
|
|
assert!(host.set_input_value(in_range, "5"));
|
|
let out_of_range = host.create_element("input");
|
|
assert!(host.set_attribute(out_of_range, "type", "number"));
|
|
assert!(host.set_attribute(out_of_range, "min", "1"));
|
|
assert!(host.set_attribute(out_of_range, "max", "10"));
|
|
assert!(host.set_attribute(out_of_range, "value", "12"));
|
|
assert!(host.set_input_value(out_of_range, "12"));
|
|
assert!(host.append_child(body, in_range));
|
|
assert!(host.append_child(body, out_of_range));
|
|
|
|
let engine = QueryEngine;
|
|
assert!(engine.matches_host(&host, in_range, ":in-range").unwrap());
|
|
assert!(
|
|
engine
|
|
.matches_host(&host, out_of_range, ":out-of-range")
|
|
.unwrap()
|
|
);
|
|
|
|
assert!(host.set_attribute(in_range, "readonly", ""));
|
|
assert!(host.set_attribute(out_of_range, "readonly", ""));
|
|
assert!(!engine.matches_host(&host, in_range, ":in-range").unwrap());
|
|
assert!(
|
|
!engine
|
|
.matches_host(&host, out_of_range, ":out-of-range")
|
|
.unwrap()
|
|
);
|
|
|
|
assert!(host.remove_attribute(in_range, "readonly"));
|
|
assert!(host.remove_attribute(out_of_range, "readonly"));
|
|
assert!(engine.matches_host(&host, in_range, ":in-range").unwrap());
|
|
assert!(
|
|
engine
|
|
.matches_host(&host, out_of_range, ":out-of-range")
|
|
.unwrap()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn dom_api_selectors_readonly_controls_are_barred_from_validity_pseudos() {
|
|
let url = url::Url::parse("https://example.test/").unwrap();
|
|
let mut host = DomHost::from_dom(NativeDom::new_html(url));
|
|
host.reset_html_document_shell();
|
|
let body = host.document_body_handle().unwrap();
|
|
let input = host.create_element("input");
|
|
assert!(host.set_attribute(input, "type", "number"));
|
|
assert!(host.set_attribute(input, "min", "1"));
|
|
assert!(host.set_attribute(input, "max", "10"));
|
|
assert!(host.set_attribute(input, "value", "12"));
|
|
assert!(host.set_input_value(input, "12"));
|
|
assert!(host.set_attribute(input, "readonly", ""));
|
|
assert!(host.append_child(body, input));
|
|
|
|
let textarea = host.create_element("textarea");
|
|
assert!(host.set_attribute(textarea, "required", ""));
|
|
assert!(host.set_attribute(textarea, "readonly", ""));
|
|
assert!(host.append_child(body, textarea));
|
|
|
|
let engine = QueryEngine;
|
|
assert!(!engine.matches_host(&host, input, ":valid").unwrap());
|
|
assert!(!engine.matches_host(&host, input, ":invalid").unwrap());
|
|
assert!(!engine.matches_host(&host, input, ":in-range").unwrap());
|
|
assert!(!engine.matches_host(&host, input, ":out-of-range").unwrap());
|
|
assert!(!engine.matches_host(&host, textarea, ":valid").unwrap());
|
|
assert!(!engine.matches_host(&host, textarea, ":invalid").unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn dom_api_selectors_default_checkbox_uses_checked_content_attribute() {
|
|
let url = url::Url::parse("https://example.test/").unwrap();
|
|
let mut host = DomHost::from_dom(NativeDom::new_html(url));
|
|
host.reset_html_document_shell();
|
|
let body = host.document_body_handle().unwrap();
|
|
let input = host.create_element("input");
|
|
assert!(host.set_attribute(input, "type", "checkbox"));
|
|
assert!(host.append_child(body, input));
|
|
|
|
let engine = QueryEngine;
|
|
assert!(!engine.matches_host(&host, input, ":default").unwrap());
|
|
assert!(host.set_checked_state(input, true));
|
|
assert!(!engine.matches_host(&host, input, ":default").unwrap());
|
|
assert!(host.set_attribute(input, "checked", ""));
|
|
assert!(engine.matches_host(&host, input, ":default").unwrap());
|
|
assert!(host.set_checked_state(input, false));
|
|
assert!(engine.matches_host(&host, input, ":default").unwrap());
|
|
assert!(host.remove_attribute(input, "checked"));
|
|
assert!(!engine.matches_host(&host, input, ":default").unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn dom_api_selectors_disabled_select_disables_option_descendants() {
|
|
let url = url::Url::parse("https://example.test/").unwrap();
|
|
let mut host = DomHost::from_dom(NativeDom::new_html(url));
|
|
host.reset_html_document_shell();
|
|
let body = host.document_body_handle().unwrap();
|
|
let select = host.create_element("select");
|
|
let optgroup = host.create_element("optgroup");
|
|
let option = host.create_element("option");
|
|
assert!(host.append_child(body, select));
|
|
assert!(host.append_child(select, optgroup));
|
|
assert!(host.append_child(optgroup, option));
|
|
|
|
let engine = QueryEngine;
|
|
assert!(!engine.matches_host(&host, optgroup, ":disabled").unwrap());
|
|
assert!(!engine.matches_host(&host, option, ":disabled").unwrap());
|
|
assert!(
|
|
!engine
|
|
.matches_host(&host, select, ":has(option:disabled)")
|
|
.unwrap()
|
|
);
|
|
|
|
assert!(host.set_attribute(select, "disabled", ""));
|
|
assert!(engine.matches_host(&host, optgroup, ":disabled").unwrap());
|
|
assert!(engine.matches_host(&host, option, ":disabled").unwrap());
|
|
assert!(
|
|
engine
|
|
.matches_host(&host, select, ":has(option:disabled)")
|
|
.unwrap()
|
|
);
|
|
|
|
assert!(host.remove_attribute(select, "disabled"));
|
|
assert!(!engine.matches_host(&host, optgroup, ":disabled").unwrap());
|
|
assert!(!engine.matches_host(&host, option, ":disabled").unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn dom_api_selectors_option_disabledness_respects_association_boundaries() {
|
|
let url = url::Url::parse("https://example.test/").unwrap();
|
|
let mut host = DomHost::from_dom(NativeDom::new_html(url));
|
|
host.reset_html_document_shell();
|
|
let body = host.document_body_handle().unwrap();
|
|
let engine = QueryEngine;
|
|
|
|
let select = host.create_element("select");
|
|
let optgroup = host.create_element("optgroup");
|
|
let div = host.create_element("div");
|
|
let option = host.create_element("option");
|
|
assert!(host.append_child(body, select));
|
|
assert!(host.append_child(select, optgroup));
|
|
assert!(host.append_child(optgroup, div));
|
|
assert!(host.append_child(div, option));
|
|
assert!(host.set_attribute(optgroup, "disabled", ""));
|
|
assert!(engine.matches_host(&host, option, ":disabled").unwrap());
|
|
|
|
let disabled_select = host.create_element("select");
|
|
let valid_optgroup = host.create_element("optgroup");
|
|
let valid_option = host.create_element("option");
|
|
let nested_optgroup = host.create_element("optgroup");
|
|
let nested_optgroup_option = host.create_element("option");
|
|
let parent_option = host.create_element("option");
|
|
let nested_option = host.create_element("option");
|
|
let hr = host.create_element("hr");
|
|
let hr_option = host.create_element("option");
|
|
let datalist = host.create_element("datalist");
|
|
let datalist_option = host.create_element("option");
|
|
assert!(host.set_attribute(disabled_select, "disabled", ""));
|
|
assert!(host.append_child(body, disabled_select));
|
|
assert!(host.append_child(disabled_select, valid_optgroup));
|
|
assert!(host.append_child(valid_optgroup, valid_option));
|
|
assert!(host.append_child(valid_optgroup, nested_optgroup));
|
|
assert!(host.append_child(nested_optgroup, nested_optgroup_option));
|
|
assert!(host.append_child(disabled_select, parent_option));
|
|
assert!(host.append_child(parent_option, nested_option));
|
|
assert!(host.append_child(disabled_select, hr));
|
|
assert!(host.append_child(hr, hr_option));
|
|
assert!(host.append_child(disabled_select, datalist));
|
|
assert!(host.append_child(datalist, datalist_option));
|
|
|
|
assert!(
|
|
engine
|
|
.matches_host(&host, valid_optgroup, ":disabled")
|
|
.unwrap()
|
|
);
|
|
assert!(
|
|
engine
|
|
.matches_host(&host, valid_option, ":disabled")
|
|
.unwrap()
|
|
);
|
|
for handle in [
|
|
nested_optgroup,
|
|
nested_optgroup_option,
|
|
nested_option,
|
|
hr_option,
|
|
datalist_option,
|
|
] {
|
|
assert!(!engine.matches_host(&host, handle, ":disabled").unwrap());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn dom_api_selectors_disabled_fieldset_disables_option_descendants() {
|
|
let url = url::Url::parse("https://example.test/").unwrap();
|
|
let mut host = DomHost::from_dom(NativeDom::new_html(url));
|
|
host.reset_html_document_shell();
|
|
let body = host.document_body_handle().unwrap();
|
|
let fieldset = host.create_element("fieldset");
|
|
let select = host.create_element("select");
|
|
let optgroup = host.create_element("optgroup");
|
|
let option = host.create_element("option");
|
|
assert!(host.append_child(body, fieldset));
|
|
assert!(host.append_child(fieldset, select));
|
|
assert!(host.append_child(select, optgroup));
|
|
assert!(host.append_child(optgroup, option));
|
|
|
|
let engine = QueryEngine;
|
|
assert!(!engine.matches_host(&host, optgroup, ":disabled").unwrap());
|
|
assert!(!engine.matches_host(&host, option, ":disabled").unwrap());
|
|
|
|
assert!(host.set_attribute(fieldset, "disabled", ""));
|
|
assert!(engine.matches_host(&host, optgroup, ":disabled").unwrap());
|
|
assert!(engine.matches_host(&host, option, ":disabled").unwrap());
|
|
|
|
assert!(host.remove_attribute(fieldset, "disabled"));
|
|
assert!(!engine.matches_host(&host, optgroup, ":disabled").unwrap());
|
|
assert!(!engine.matches_host(&host, option, ":disabled").unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn dom_api_selectors_radio_indeterminate_walks_deep_dom_iteratively() {
|
|
let url = url::Url::parse("https://example.test/").unwrap();
|
|
let mut host = DomHost::from_dom(NativeDom::new_html(url));
|
|
host.reset_html_document_shell();
|
|
let body = host.document_body_handle().unwrap();
|
|
let target = host.create_element("input");
|
|
assert!(host.set_attribute(target, "type", "radio"));
|
|
assert!(host.set_attribute(target, "name", "group"));
|
|
assert!(host.append_child(body, target));
|
|
|
|
let mut parent = body;
|
|
for _ in 0..4096 {
|
|
let child = host.create_element("div");
|
|
assert!(host.append_child(parent, child));
|
|
parent = child;
|
|
}
|
|
let checked = host.create_element("input");
|
|
assert!(host.set_attribute(checked, "type", "radio"));
|
|
assert!(host.set_attribute(checked, "name", "group"));
|
|
assert!(host.set_attribute(checked, "checked", ""));
|
|
assert!(host.append_child(parent, checked));
|
|
|
|
let engine = QueryEngine;
|
|
assert!(
|
|
!engine
|
|
.matches_host(&host, target, ":indeterminate")
|
|
.unwrap()
|
|
);
|
|
assert!(engine.matches_host(&host, checked, ":checked").unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn dom_api_selectors_default_submit_walks_deep_dom_iteratively() {
|
|
let url = url::Url::parse("https://example.test/").unwrap();
|
|
let mut host = DomHost::from_dom(NativeDom::new_html(url));
|
|
host.reset_html_document_shell();
|
|
let body = host.document_body_handle().unwrap();
|
|
let form = host.create_element("form");
|
|
assert!(host.append_child(body, form));
|
|
|
|
let mut parent = form;
|
|
for _ in 0..4096 {
|
|
let child = host.create_element("div");
|
|
assert!(host.append_child(parent, child));
|
|
parent = child;
|
|
}
|
|
let submit = host.create_element("button");
|
|
assert!(host.append_child(parent, submit));
|
|
|
|
let engine = QueryEngine;
|
|
assert!(engine.matches_host(&host, submit, ":default").unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn target_pseudo_class_matches_current_fragment_target() {
|
|
let url = url::Url::parse("https://example.test/page.html#target").unwrap();
|
|
let mut host = DomHost::from_dom(NativeDom::new_html(url));
|
|
host.reset_html_document_shell();
|
|
let body = host.document_body_handle().unwrap();
|
|
let target = host.create_element("div");
|
|
assert!(host.set_attribute(target, "id", "target"));
|
|
let other = host.create_element("div");
|
|
assert!(host.set_attribute(other, "id", "other"));
|
|
assert!(host.append_child(body, target));
|
|
assert!(host.append_child(body, other));
|
|
|
|
let engine = QueryEngine;
|
|
assert_eq!(
|
|
engine.query_selector_host(&host, ":target").unwrap(),
|
|
Some(target)
|
|
);
|
|
assert!(engine.matches_host(&host, target, ":target").unwrap());
|
|
assert!(!engine.matches_host(&host, other, ":target").unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn target_pseudo_class_matches_percent_decoded_anchor_name() {
|
|
let url = url::Url::parse("https://example.test/page.html#caf%C3%A9").unwrap();
|
|
let mut host = DomHost::from_dom(NativeDom::new_html(url));
|
|
host.reset_html_document_shell();
|
|
let body = host.document_body_handle().unwrap();
|
|
let target = host.create_element("a");
|
|
assert!(host.set_attribute(target, "name", "café"));
|
|
assert!(host.append_child(body, target));
|
|
|
|
let engine = QueryEngine;
|
|
assert_eq!(
|
|
engine.query_selector_host(&host, "a:target").unwrap(),
|
|
Some(target)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn target_pseudo_class_matches_percent_decoded_element_id() {
|
|
let url = url::Url::parse("https://example.test/page.html#foo%20bar").unwrap();
|
|
let mut host = DomHost::from_dom(NativeDom::new_html(url));
|
|
host.reset_html_document_shell();
|
|
let body = host.document_body_handle().unwrap();
|
|
let target = host.create_element("div");
|
|
assert!(host.set_attribute(target, "id", "foo bar"));
|
|
assert!(host.append_child(body, target));
|
|
|
|
let engine = QueryEngine;
|
|
assert_eq!(
|
|
engine.query_selector_host(&host, ":target").unwrap(),
|
|
Some(target)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn simple_class_descendant_selector_uses_stylo_semantics() {
|
|
let url = url::Url::parse("https://example.test/").unwrap();
|
|
let mut host = DomHost::from_dom(NativeDom::new_html(url));
|
|
host.reset_html_document_shell();
|
|
let body = host.document_body_handle().unwrap();
|
|
let slide = host.create_element("div");
|
|
assert!(host.set_attribute(slide, "class", "slick-slide active"));
|
|
let image = host.create_element("img");
|
|
let other = host.create_element("img");
|
|
assert!(host.append_child(slide, image));
|
|
assert!(host.append_child(body, slide));
|
|
assert!(host.append_child(body, other));
|
|
|
|
let engine = QueryEngine;
|
|
assert_eq!(
|
|
engine
|
|
.query_selector_all_in_host(&host, body, ".slick-slide")
|
|
.unwrap(),
|
|
vec![slide]
|
|
);
|
|
assert_eq!(
|
|
engine
|
|
.query_selector_all_in_host(&host, body, ".slick-slide img")
|
|
.unwrap(),
|
|
vec![image]
|
|
);
|
|
assert!(
|
|
engine
|
|
.matches_host(&host, image, ".slick-slide img")
|
|
.unwrap()
|
|
);
|
|
assert!(
|
|
!engine
|
|
.matches_host(&host, other, ".slick-slide img")
|
|
.unwrap()
|
|
);
|
|
assert_eq!(
|
|
engine
|
|
.closest_host(&host, image, ".slick-slide img")
|
|
.unwrap(),
|
|
Some(image)
|
|
);
|
|
}
|
|
}
|