context(":where() pseudo-class")
test_that(":where() generates correct XPath", {
xpath <- function(css) {
css_to_xpath(css, prefix = "")
}
# Simple :where() with single selector
expect_that(xpath("div:where(p)"),
equals("div[((name() = 'p'))]"))
# :where() with class selector
expect_that(xpath("div:where(.foo)"),
equals("div[((@class and contains(concat(' ', normalize-space(@class), ' '), ' foo ')))]"))
# :where() with ID selector
expect_that(xpath("section:where(#main)"),
equals("section[((@id = 'main'))]"))
# :where() with attribute selector
expect_that(xpath("input:where([required])"),
equals("input[((@required))]"))
# :where() with multiple selectors (OR logic)
expect_that(xpath("div:where(p, span)"),
equals("div[((name() = 'p')) or ((name() = 'span'))]"))
# :where() with element and class (both conditions must match)
expect_that(xpath("*:where(div.content)"),
equals("*[((@class and contains(concat(' ', normalize-space(@class), ' '), ' content ')) and (name() = 'div'))]"))
# Multiple :where() selectors - currently treated as OR, not AND
# Note: parser combines them into a single :where()
expect_that(xpath("div:where(p):where(span)"),
equals("div[((name() = 'p')) or ((name() = 'span'))]"))
# :where() on universal selector
expect_that(xpath("*:where(.highlight)"),
equals("*[((@class and contains(concat(' ', normalize-space(@class), ' '), ' highlight ')))]"))
# :where() with multiple classes
expect_that(xpath("div:where(.foo, .bar)"),
equals("div[((@class and contains(concat(' ', normalize-space(@class), ' '), ' foo '))) or ((@class and contains(concat(' ', normalize-space(@class), ' '), ' bar ')))]"))
# Complex: :where() with mix of selectors
expect_that(xpath("p:where(.highlight, #special, [data-key])"),
equals("p[((@class and contains(concat(' ', normalize-space(@class), ' '), ' highlight '))) or ((@id = 'special')) or ((@data-key))]"))
})
test_that(":where() works correctly with XML documents", {
library(XML)
html <- paste0(
'',
' Div 1
',
' ',
' Para 1
',
' Para 2
',
' Span 1',
' ',
''
)
doc <- xmlRoot(xmlParse(html))
get_ids <- function(css) {
results <- querySelectorAll(doc, css)
sapply(results, function(x) xmlGetAttr(x, "id"))
}
# Elements matching div OR p (via :where)
expect_that(get_ids("*:where(div, p)"),
equals(c("d1", "d2", "p1", "p2")))
# Elements with class content (any element type)
expect_that(get_ids("*:where(.content)"),
equals(c("d1", "p1", "s1")))
# Div elements that are either content or sidebar
expect_that(get_ids("div:where(.content, .sidebar)"),
equals(c("d1", "d2")))
# Elements matching specific ID
# Note: returns all ancestors in XML, so we check for inclusion
ids <- get_ids("*:where(#p1)")
expect_that("p1" %in% ids, equals(TRUE))
# :where() with element that has specific class
expect_that(get_ids("*:where(p.highlight)"),
equals("p2"))
# :where() matches nothing if conditions don't align
expect_that(length(querySelectorAll(doc, "div:where(p)")),
equals(0))
})
test_that(":where() works correctly with xml2 documents", {
library(xml2)
html <- paste0(
'',
' Div 1
',
' ',
' Para 1
',
' Para 2
',
' Span 1',
' ',
''
)
doc <- read_xml(html)
get_ids <- function(css) {
results <- querySelectorAll(doc, css)
xml_attr(results, "id")
}
# Elements matching div OR p (via :where)
expect_that(get_ids("*:where(div, p)"),
equals(c("d1", "d2", "p1", "p2")))
# Elements with class content (any element type)
expect_that(get_ids("*:where(.content)"),
equals(c("d1", "p1", "s1")))
# Div elements that are either content or sidebar
expect_that(get_ids("div:where(.content, .sidebar)"),
equals(c("d1", "d2")))
# Elements matching specific ID
# Note: returns all ancestors, so we check for inclusion
ids <- get_ids("*:where(#p1)")
expect_that("p1" %in% ids, equals(TRUE))
# :where() with element that has specific class
expect_that(get_ids("*:where(p.highlight)"),
equals("p2"))
# :where() matches nothing if conditions don't align
expect_that(length(querySelectorAll(doc, "div:where(p)")),
equals(0))
})
test_that(":where() has zero specificity", {
library(XML)
html <- paste0(
'',
' Content
',
''
)
doc <- xmlRoot(xmlParse(html))
# All of these should match the same element
# :where() doesn't add specificity regardless of what's inside
expect_that(length(querySelectorAll(doc, "div:where(#test)")), equals(1))
expect_that(length(querySelectorAll(doc, "div:where(.foo)")), equals(1))
expect_that(length(querySelectorAll(doc, ":where(div)")), equals(1))
expect_that(length(querySelectorAll(doc, ":where(#test, .foo, div)")), equals(1))
# Specificity is handled in parser/specificity calculation
# Here we just verify matching works
})
test_that(":where() handles edge cases correctly", {
library(XML)
# Empty document case
html1 <- ''
doc1 <- xmlRoot(xmlParse(html1))
expect_that(length(querySelectorAll(doc1, "*:where(div)")), equals(0))
# Multiple classes
html2 <- paste0(
'',
' A
',
' B
',
' C
',
''
)
doc2 <- xmlRoot(xmlParse(html2))
# Divs with foo OR bar class
result <- querySelectorAll(doc2, "div:where(.foo, .bar)")
expect_that(length(result), equals(3))
# :where() with universal selector inside
html4 <- ''
doc4 <- xmlRoot(xmlParse(html4))
# This matches elements that are any type (essentially all elements plus root)
result3 <- querySelectorAll(doc4, "*:where(*)")
# Returns root plus all descendants
expect_that(length(result3) >= 3, equals(TRUE))
})
test_that(":where() works with querySelector (returns first match)", {
library(xml2)
html <- paste0(
'',
' First
',
' Second
',
' Third',
''
)
doc <- read_xml(html)
# Should return first element with class foo
result <- querySelector(doc, "*:where(.foo)")
expect_that(xml_attr(result, "id"), equals("d1"))
# Should return first div or p
result2 <- querySelector(doc, "*:where(div, p)")
expect_that(xml_attr(result2, "id"), equals("d1"))
# Should return NULL when no match
result_none <- querySelector(doc, "*:where(article)")
expect_that(result_none, equals(NULL))
})
test_that(":where() and :is() behave similarly in matching", {
library(XML)
html <- paste0(
'',
' Div
',
' Para
',
' Span',
''
)
doc <- xmlRoot(xmlParse(html))
get_ids <- function(css) {
results <- querySelectorAll(doc, css)
sapply(results, function(x) xmlGetAttr(x, "id"))
}
# :where() and :is() should match the same elements
# (only difference is specificity, which doesn't affect matching)
where_result <- get_ids("*:where(div, p)")
is_result <- get_ids("*:is(div, p)")
expect_that(where_result, equals(is_result))
where_result2 <- get_ids("*:where(.content)")
is_result2 <- get_ids("*:is(.content)")
expect_that(where_result2, equals(is_result2))
})
test_that(":where() can be combined with other selectors", {
library(xml2)
html <- paste0(
'',
' ',
' Div 1
',
' Para 1
',
' ',
' ',
''
)
doc <- read_xml(html)
get_ids <- function(css) {
results <- querySelectorAll(doc, css)
xml_attr(results, "id")
}
# Descendant combinator: section containing divs or ps
# Note: returns all matching descendants including ancestors
ids <- get_ids("section *:where(div, p)")
expect_that("d1" %in% ids && "p1" %in% ids, equals(TRUE))
# Class selector before :where()
# Elements with class content that are divs or ps
# (all 4 elements match: d1,p1 have .content, and :where checks div|p)
result <- get_ids(".content:where(div, p)")
expect_that("d1" %in% result && "p1" %in% result, equals(TRUE))
# Child combinator
expect_that(get_ids("section > *:where(.content)"),
equals(c("d1", "p1")))
})