tests/testthat/test-nth-child.R

context(":nth-child() and :nth-last-child() pseudo-classes")

test_that(":nth-child() generates correct XPath", {
    xpath <- function(css) {
        css_to_xpath(css, prefix = "")
    }

    # :nth-child(1) - first child
    result <- xpath("li:nth-child(1)")
    expect_true(grepl("count\\(preceding-sibling::\\*\\) = 0", result))

    # :nth-child(2) - second child
    result <- xpath("li:nth-child(2)")
    expect_true(grepl("count\\(preceding-sibling::\\*\\) = 1", result))

    # :nth-child(odd) - odd children
    result <- xpath("li:nth-child(odd)")
    expect_true(grepl("count\\(preceding-sibling::\\*\\)", result))
    expect_true(grepl("mod 2", result))

    # :nth-child(even) - even children
    result <- xpath("li:nth-child(even)")
    expect_true(grepl("count\\(preceding-sibling::\\*\\)", result))
    expect_true(grepl("mod 2", result))

    # :nth-child(2n) - every 2nd child (even)
    result <- xpath("li:nth-child(2n)")
    expect_true(grepl("count\\(preceding-sibling::\\*\\)", result))

    # :nth-child(3n+1) - every 3rd starting from 1st
    result <- xpath("li:nth-child(3n+1)")
    expect_true(grepl("count\\(preceding-sibling::\\*\\)", result))

    # :nth-child(n) - all children (simplifies to just the element)
    result <- xpath("li:nth-child(n)")
    expect_that(result, equals("li"))

    # :nth-child(-n+3) - first 3 children
    result <- xpath("li:nth-child(-n+3)")
    expect_true(grepl("count\\(preceding-sibling::\\*\\)", result))
})

test_that(":nth-last-child() generates correct XPath", {
    xpath <- function(css) {
        css_to_xpath(css, prefix = "")
    }

    # :nth-last-child(1) - last child
    result <- xpath("li:nth-last-child(1)")
    expect_true(grepl("count\\(following-sibling::\\*\\) = 0", result))

    # :nth-last-child(2) - second from last
    result <- xpath("li:nth-last-child(2)")
    expect_true(grepl("count\\(following-sibling::\\*\\) = 1", result))

    # :nth-last-child(odd) - odd from end
    result <- xpath("li:nth-last-child(odd)")
    expect_true(grepl("count\\(following-sibling::\\*\\)", result))
    expect_true(grepl("mod 2", result))

    # :nth-last-child(even) - even from end
    result <- xpath("li:nth-last-child(even)")
    expect_true(grepl("count\\(following-sibling::\\*\\)", result))
    expect_true(grepl("mod 2", result))

    # :nth-last-child(-n+2) - last 2 children
    result <- xpath("li:nth-last-child(-n+2)")
    expect_true(grepl("count\\(following-sibling::\\*\\)", result))
})

test_that(":nth-child() works correctly with XML documents", {
    library(XML)

    html <- paste0(
        '<root>',
        '  <ul id="list1">',
        '    <li id="li1" class="odd">Item 1</li>',
        '    <li id="li2" class="even">Item 2</li>',
        '    <li id="li3" class="odd">Item 3</li>',
        '    <li id="li4" class="even">Item 4</li>',
        '    <li id="li5" class="odd">Item 5</li>',
        '  </ul>',
        '  <ul id="list2">',
        '    <li id="li6">A</li>',
        '    <li id="li7">B</li>',
        '    <li id="li8">C</li>',
        '  </ul>',
        '</root>'
    )

    doc <- xmlRoot(xmlParse(html))

    get_ids <- function(css) {
        results <- querySelectorAll(doc, css)
        sapply(results, function(x) xmlGetAttr(x, "id"))
    }

    # First child
    expect_that(get_ids("li:nth-child(1)"),
                equals(c("li1", "li6")))

    # Second child
    expect_that(get_ids("li:nth-child(2)"),
                equals(c("li2", "li7")))

    # Third child
    expect_that(get_ids("li:nth-child(3)"),
                equals(c("li3", "li8")))

    # Odd children (1, 3, 5)
    expect_that(get_ids("li:nth-child(odd)"),
                equals(c("li1", "li3", "li5", "li6", "li8")))

    # Even children (2, 4)
    expect_that(get_ids("li:nth-child(even)"),
                equals(c("li2", "li4", "li7")))

    # Every 2nd child starting from 2 (same as even)
    expect_that(get_ids("li:nth-child(2n)"),
                equals(c("li2", "li4", "li7")))

    # Every 2nd child starting from 1 (same as odd)
    expect_that(get_ids("li:nth-child(2n+1)"),
                equals(c("li1", "li3", "li5", "li6", "li8")))

    # Every 3rd child starting from 1 (1, 4)
    expect_that(get_ids("li:nth-child(3n+1)"),
                equals(c("li1", "li4", "li6")))

    # Every 3rd child starting from 2 (2, 5)
    expect_that(get_ids("li:nth-child(3n+2)"),
                equals(c("li2", "li5", "li7")))

    # First 3 children
    expect_that(get_ids("li:nth-child(-n+3)"),
                equals(c("li1", "li2", "li3", "li6", "li7", "li8")))

    # All children (n matches all positive integers)
    all_ids <- get_ids("li:nth-child(n)")
    expect_that(length(all_ids), equals(8))
})

test_that(":nth-last-child() works correctly with XML documents", {
    library(XML)

    html <- paste0(
        '<root>',
        '  <ul id="list1">',
        '    <li id="li1">Item 1</li>',
        '    <li id="li2">Item 2</li>',
        '    <li id="li3">Item 3</li>',
        '    <li id="li4">Item 4</li>',
        '    <li id="li5">Item 5</li>',
        '  </ul>',
        '  <ul id="list2">',
        '    <li id="li6">A</li>',
        '    <li id="li7">B</li>',
        '    <li id="li8">C</li>',
        '  </ul>',
        '</root>'
    )

    doc <- xmlRoot(xmlParse(html))

    get_ids <- function(css) {
        results <- querySelectorAll(doc, css)
        sapply(results, function(x) xmlGetAttr(x, "id"))
    }

    # Last child
    expect_that(get_ids("li:nth-last-child(1)"),
                equals(c("li5", "li8")))

    # Second from last
    expect_that(get_ids("li:nth-last-child(2)"),
                equals(c("li4", "li7")))

    # Third from last
    expect_that(get_ids("li:nth-last-child(3)"),
                equals(c("li3", "li6")))

    # Odd from end (last=1, 3rd-last=3, 5th-last=5)
    expect_that(get_ids("li:nth-last-child(odd)"),
                equals(c("li1", "li3", "li5", "li6", "li8")))

    # Even from end (2nd-last=2, 4th-last=4)
    expect_that(get_ids("li:nth-last-child(even)"),
                equals(c("li2", "li4", "li7")))

    # Last 2 children
    expect_that(get_ids("li:nth-last-child(-n+2)"),
                equals(c("li4", "li5", "li7", "li8")))

    # Last 3 children
    expect_that(get_ids("li:nth-last-child(-n+3)"),
                equals(c("li3", "li4", "li5", "li6", "li7", "li8")))
})

test_that(":nth-child() works correctly with xml2 documents", {
    library(xml2)

    html <- paste0(
        '<root>',
        '  <ul id="list1">',
        '    <li id="li1">Item 1</li>',
        '    <li id="li2">Item 2</li>',
        '    <li id="li3">Item 3</li>',
        '    <li id="li4">Item 4</li>',
        '    <li id="li5">Item 5</li>',
        '  </ul>',
        '  <ul id="list2">',
        '    <li id="li6">A</li>',
        '    <li id="li7">B</li>',
        '    <li id="li8">C</li>',
        '  </ul>',
        '</root>'
    )

    doc <- read_xml(html)

    get_ids <- function(css) {
        results <- querySelectorAll(doc, css)
        xml_attr(results, "id")
    }

    # First child
    expect_that(get_ids("li:nth-child(1)"),
                equals(c("li1", "li6")))

    # Second child
    expect_that(get_ids("li:nth-child(2)"),
                equals(c("li2", "li7")))

    # Odd children
    expect_that(get_ids("li:nth-child(odd)"),
                equals(c("li1", "li3", "li5", "li6", "li8")))

    # Even children
    expect_that(get_ids("li:nth-child(even)"),
                equals(c("li2", "li4", "li7")))

    # First 3 children
    expect_that(get_ids("li:nth-child(-n+3)"),
                equals(c("li1", "li2", "li3", "li6", "li7", "li8")))
})

test_that(":nth-last-child() works correctly with xml2 documents", {
    library(xml2)

    html <- paste0(
        '<root>',
        '  <ul id="list1">',
        '    <li id="li1">Item 1</li>',
        '    <li id="li2">Item 2</li>',
        '    <li id="li3">Item 3</li>',
        '    <li id="li4">Item 4</li>',
        '    <li id="li5">Item 5</li>',
        '  </ul>',
        '</root>'
    )

    doc <- read_xml(html)

    get_ids <- function(css) {
        results <- querySelectorAll(doc, css)
        xml_attr(results, "id")
    }

    # Last child
    expect_that(get_ids("li:nth-last-child(1)"),
                equals("li5"))

    # Second from last
    expect_that(get_ids("li:nth-last-child(2)"),
                equals("li4"))

    # Last 2 children
    expect_that(get_ids("li:nth-last-child(-n+2)"),
                equals(c("li4", "li5")))
})

test_that(":nth-child() and :nth-last-child() can be combined", {
    library(XML)

    html <- paste0(
        '<root>',
        '  <ul>',
        '    <li id="li1">1</li>',
        '    <li id="li2">2</li>',
        '    <li id="li3">3</li>',
        '    <li id="li4">4</li>',
        '    <li id="li5">5</li>',
        '  </ul>',
        '</root>'
    )

    doc <- xmlRoot(xmlParse(html))

    get_ids <- function(css) {
        results <- querySelectorAll(doc, css)
        sapply(results, function(x) xmlGetAttr(x, "id"))
    }

    # Second child AND second from last (middle element in list of 5)
    expect_that(get_ids("li:nth-child(2):nth-last-child(4)"),
                equals("li2"))

    # Middle element (3rd child AND 3rd from last)
    expect_that(get_ids("li:nth-child(3):nth-last-child(3)"),
                equals("li3"))

    # First child that's also last child (only child)
    # This won't match in our test case since we have 5 items
    expect_that(length(querySelectorAll(doc, "li:nth-child(1):nth-last-child(1)")),
                equals(0))
})

test_that(":nth-child() edge cases", {
    library(XML)

    # Empty list
    html1 <- '<root><ul id="empty"></ul></root>'
    doc1 <- xmlRoot(xmlParse(html1))
    expect_that(length(querySelectorAll(doc1, "li:nth-child(1)")),
                equals(0))

    # Single child
    html2 <- '<root><ul><li id="only">Only</li></ul></root>'
    doc2 <- xmlRoot(xmlParse(html2))

    # Should match as first child
    result <- querySelectorAll(doc2, "li:nth-child(1)")
    expect_that(length(result), equals(1))
    expect_that(xmlGetAttr(result[[1]], "id"), equals("only"))

    # Should also match as last child
    result2 <- querySelectorAll(doc2, "li:nth-last-child(1)")
    expect_that(length(result2), equals(1))
    expect_that(xmlGetAttr(result2[[1]], "id"), equals("only"))

    # Mixed element types
    html3 <- paste0(
        '<root>',
        '  <div>',
        '    <p id="p1">Para</p>',
        '    <div id="d1">Div</div>',
        '    <p id="p2">Para</p>',
        '    <span id="s1">Span</span>',
        '  </div>',
        '</root>'
    )
    doc3 <- xmlRoot(xmlParse(html3))

    get_ids <- function(css) {
        results <- querySelectorAll(doc3, css)
        sapply(results, function(x) xmlGetAttr(x, "id"))
    }

    # First child (p element that is first child)
    expect_that(get_ids("p:nth-child(1)"),
                equals("p1"))

    # Second child (div element that is second child)
    expect_that(get_ids("div:nth-child(2)"),
                equals("d1"))

    # All p elements that are odd children
    expect_that(get_ids("p:nth-child(odd)"),
                equals(c("p1", "p2")))
})

test_that(":nth-child() with querySelector returns first match", {
    library(xml2)

    html <- paste0(
        '<root>',
        '  <ul>',
        '    <li id="li1">1</li>',
        '    <li id="li2">2</li>',
        '  </ul>',
        '  <ul>',
        '    <li id="li3">3</li>',
        '    <li id="li4">4</li>',
        '  </ul>',
        '</root>'
    )

    doc <- read_xml(html)

    # Should return first element that's a first child (li1)
    result <- querySelector(doc, "li:nth-child(1)")
    expect_that(xml_attr(result, "id"), equals("li1"))

    # Should return first element that's a second child (li2)
    result2 <- querySelector(doc, "li:nth-child(2)")
    expect_that(xml_attr(result2, "id"), equals("li2"))
})

test_that(":nth-child() with different element types", {
    library(XML)

    # Test that :nth-child counts all siblings, not just same type
    html <- paste0(
        '<root>',
        '  <div>',
        '    <h1 id="h1">Heading</h1>',
        '    <p id="p1">Para 1</p>',
        '    <p id="p2">Para 2</p>',
        '    <span id="s1">Span</span>',
        '  </div>',
        '</root>'
    )

    doc <- xmlRoot(xmlParse(html))

    get_ids <- function(css) {
        results <- querySelectorAll(doc, css)
        sapply(results, function(x) xmlGetAttr(x, "id"))
    }

    # p:nth-child(2) - p that is 2nd child (p1)
    expect_that(get_ids("p:nth-child(2)"),
                equals("p1"))

    # p:nth-child(3) - p that is 3rd child (p2)
    expect_that(get_ids("p:nth-child(3)"),
                equals("p2"))

    # p:nth-child(1) - p that is first child (none)
    expect_that(length(querySelectorAll(doc, "p:nth-child(1)")),
                equals(0))

    # span:nth-child(4) - span that is 4th child (s1)
    expect_that(get_ids("span:nth-child(4)"),
                equals("s1"))
})

test_that(":nth-child() with complex selectors", {
    library(xml2)

    html <- paste0(
        '<root>',
        '  <ul class="menu">',
        '    <li id="li1" class="item">First</li>',
        '    <li id="li2" class="item active">Second</li>',
        '    <li id="li3" class="item">Third</li>',
        '  </ul>',
        '</root>'
    )

    doc <- read_xml(html)

    get_ids <- function(css) {
        results <- querySelectorAll(doc, css)
        xml_attr(results, "id")
    }

    # Class selector with :nth-child
    expect_that(get_ids(".item:nth-child(1)"),
                equals("li1"))

    # Multiple classes with :nth-child
    expect_that(get_ids(".item.active:nth-child(2)"),
                equals("li2"))

    # Descendant combinator with :nth-child
    expect_that(get_ids(".menu li:nth-child(2)"),
                equals("li2"))

    # Child combinator with :nth-child
    expect_that(get_ids(".menu > li:nth-child(3)"),
                equals("li3"))
})

test_that(":nth-child() early-exit condition 1: a=1, b-1<=0 (matches all)", {
    library(XML)

    html <- paste0(
        '<root>',
        '  <ul>',
        '    <li id="li1" class="item">1</li>',
        '    <li id="li2" class="item">2</li>',
        '    <li id="li3" class="item">3</li>',
        '    <li id="li4">4</li>',
        '  </ul>',
        '</root>'
    )

    doc <- xmlRoot(xmlParse(html))

    get_ids <- function(css) {
        results <- querySelectorAll(doc, css)
        sapply(results, function(x) xmlGetAttr(x, "id"))
    }

    # :nth-child(n) -> a=1, b=0, b-1=-1<=0 -> matches all
    expect_that(get_ids("li:nth-child(n)"),
                equals(c("li1", "li2", "li3", "li4")))

    # :nth-child(1n+0) -> a=1, b=0, b-1=-1<=0 -> matches all
    expect_that(get_ids("li:nth-child(1n+0)"),
                equals(c("li1", "li2", "li3", "li4")))

    # :nth-child(n+1) -> a=1, b=1, b-1=0<=0 -> matches all
    expect_that(get_ids("li:nth-child(n+1)"),
                equals(c("li1", "li2", "li3", "li4")))

    # :nth-child(1n+1) -> a=1, b=1, b-1=0<=0 -> matches all
    expect_that(get_ids("li:nth-child(1n+1)"),
                equals(c("li1", "li2", "li3", "li4")))

    # :nth-child(n-1) -> a=1, b=-1, b-1=-2<=0 -> matches all
    expect_that(get_ids("li:nth-child(n-1)"),
                equals(c("li1", "li2", "li3", "li4")))

    # :nth-child(n-5) -> a=1, b=-5, b-1=-6<=0 -> matches all
    expect_that(get_ids("li:nth-child(n-5)"),
                equals(c("li1", "li2", "li3", "li4")))
})

test_that(":nth-child() early-exit condition 1 with selector list", {
    library(xml2)

    html <- paste0(
        '<root>',
        '  <ul>',
        '    <li id="li1" class="item">1</li>',
        '    <li id="li2" class="item">2</li>',
        '    <li id="li3">3</li>',
        '    <li id="li4" class="item">4</li>',
        '  </ul>',
        '</root>'
    )

    doc <- read_xml(html)

    get_ids <- function(css) {
        results <- querySelectorAll(doc, css)
        xml_attr(results, "id")
    }

    # :nth-child(n of .item) -> a=1, b=0 -> but filtered by .item
    expect_that(get_ids("li:nth-child(n of .item)"),
                equals(c("li1", "li2", "li4")))

    # :nth-child(1n+0 of .item) -> same as above
    expect_that(get_ids("li:nth-child(1n+0 of .item)"),
                equals(c("li1", "li2", "li4")))

    # :nth-child(n+1 of .item) -> a=1, b=1 -> filtered by .item
    expect_that(get_ids("li:nth-child(n+1 of .item)"),
                equals(c("li1", "li2", "li4")))
})

test_that(":nth-last-child() early-exit condition 1: a=1, b-1<=0", {
    library(XML)

    html <- paste0(
        '<root>',
        '  <ul>',
        '    <li id="li1">1</li>',
        '    <li id="li2" class="special">2</li>',
        '    <li id="li3">3</li>',
        '  </ul>',
        '</root>'
    )

    doc <- xmlRoot(xmlParse(html))

    get_ids <- function(css) {
        results <- querySelectorAll(doc, css)
        sapply(results, function(x) xmlGetAttr(x, "id"))
    }

    # :nth-last-child(n) -> a=1, b=0, b-1=-1<=0 -> matches all
    expect_that(get_ids("li:nth-last-child(n)"),
                equals(c("li1", "li2", "li3")))

    # :nth-last-child(n+1) -> a=1, b=1, b-1=0<=0 -> matches all
    expect_that(get_ids("li:nth-last-child(n+1)"),
                equals(c("li1", "li2", "li3")))

    # :nth-last-child(n of .special) -> filtered by .special
    expect_that(get_ids("li:nth-last-child(n of .special)"),
                equals("li2"))
})

test_that(":nth-child() early-exit condition 2: a<0, b-1<0 (matches none)", {
    library(XML)

    html <- paste0(
        '<root>',
        '  <ul>',
        '    <li id="li1" class="item">1</li>',
        '    <li id="li2" class="item">2</li>',
        '    <li id="li3">3</li>',
        '  </ul>',
        '</root>'
    )

    doc <- xmlRoot(xmlParse(html))

    # :nth-child(-n) -> a=-1, b=0, b-1=-1<0 -> impossible, matches none
    expect_that(length(querySelectorAll(doc, "li:nth-child(-n)")),
                equals(0))

    # :nth-child(-n-1) -> a=-1, b=-1, b-1=-2<0 -> impossible, matches none
    expect_that(length(querySelectorAll(doc, "li:nth-child(-n-1)")),
                equals(0))

    # :nth-child(-2n-1) -> a=-2, b=-1, b-1=-2<0 -> impossible, matches none
    expect_that(length(querySelectorAll(doc, "li:nth-child(-2n-1)")),
                equals(0))

    # :nth-child(-3n-5) -> a=-3, b=-5, b-1=-6<0 -> impossible, matches none
    expect_that(length(querySelectorAll(doc, "li:nth-child(-3n-5)")),
                equals(0))

    # Verify XPath contains "0" condition
    xpath <- css_to_xpath("li:nth-child(-n)")
    expect_true(grepl("\\[.*0.*\\]", xpath))
})

test_that(":nth-child() early-exit condition 2 with selector list", {
    library(xml2)

    html <- paste0(
        '<root>',
        '  <ul>',
        '    <li id="li1" class="item">1</li>',
        '    <li id="li2" class="item">2</li>',
        '  </ul>',
        '</root>'
    )

    doc <- read_xml(html)

    # :nth-child(-n of .item) -> a=-1, b=0 -> impossible even with selector
    expect_that(length(querySelectorAll(doc, "li:nth-child(-n of .item)")),
                equals(0))

    # :nth-child(-2n-1 of .item) -> a=-2, b=-1 -> impossible
    expect_that(length(querySelectorAll(doc, "li:nth-child(-2n-1 of .item)")),
                equals(0))

    # Verify XPath contains both "0" condition and selector check
    xpath <- css_to_xpath("li:nth-child(-n of .item)")
    expect_true(grepl("0", xpath))
    expect_true(grepl("item", xpath))
})

test_that(":nth-last-child() early-exit condition 2: a<0, b-1<0", {
    library(XML)

    html <- paste0(
        '<root>',
        '  <ul>',
        '    <li id="li1">1</li>',
        '    <li id="li2" class="special">2</li>',
        '    <li id="li3">3</li>',
        '  </ul>',
        '</root>'
    )

    doc <- xmlRoot(xmlParse(html))

    # :nth-last-child(-n) -> a=-1, b=0, b-1=-1<0 -> impossible
    expect_that(length(querySelectorAll(doc, "li:nth-last-child(-n)")),
                equals(0))

    # :nth-last-child(-n-1) -> a=-1, b=-1, b-1=-2<0 -> impossible
    expect_that(length(querySelectorAll(doc, "li:nth-last-child(-n-1)")),
                equals(0))

    # :nth-last-child(-n of .special) -> impossible even with selector
    expect_that(length(querySelectorAll(doc, "li:nth-last-child(-n of .special)")),
                equals(0))
})

test_that(":nth-child() boundary between early-exit conditions", {
    library(xml2)

    html <- paste0(
        '<root>',
        '  <ul>',
        '    <li id="li1">1</li>',
        '    <li id="li2">2</li>',
        '    <li id="li3">3</li>',
        '  </ul>',
        '</root>'
    )

    doc <- read_xml(html)

    get_ids <- function(css) {
        results <- querySelectorAll(doc, css)
        xml_attr(results, "id")
    }

    # :nth-child(-n+0) -> a=-1, b=0, b-1=-1 -> NOT early-exit (b-1<0 but a<0 not b-1<0)
    # This should match nothing (0 or fewer siblings)
    expect_that(length(querySelectorAll(doc, "li:nth-child(-n+0)")),
                equals(0))

    # :nth-child(-n+1) -> a=-1, b=1, b-1=0 -> NOT early-exit (b-1 not <0)
    # This should match first child only
    expect_that(get_ids("li:nth-child(-n+1)"),
                equals("li1"))

    # :nth-child(-n+2) -> a=-1, b=2, b-1=1 -> matches first 2
    expect_that(get_ids("li:nth-child(-n+2)"),
                equals(c("li1", "li2")))

    # :nth-child(-2n+2) -> a=-2, b=2, b-1=1 -> matches 2nd child
    expect_that(get_ids("li:nth-child(-2n+2)"),
                equals("li2"))

    # :nth-child(-2n+0) -> a=-2, b=0, b-1=-1<0 -> early-exit condition 2
    expect_that(length(querySelectorAll(doc, "li:nth-child(-2n+0)")),
                equals(0))
})

Try the selectr package in your browser

Any scripts or data that you put into this service are public.

selectr documentation built on Nov. 21, 2025, 5:07 p.m.