335 lines
12 KiB
Python
335 lines
12 KiB
Python
"""Test suite which tests the entire logic together (tokenization, parsing, matching).
|
|
|
|
This suite doesn't focus on any individual components, only going from selector string -> matching.
|
|
Some of the test cases in this suite will technically overlap those in the other suites, this
|
|
is intentional, as this suite is also useful for testing other implementations of the matching
|
|
(not just my specific one), unlike with the other suites, which work with my specific AST/parser.
|
|
|
|
This suite is written with pytest to allow for easier parametrizations and provide better output.
|
|
Generally, this suite contains the same tests as the original python discord's test suite, with
|
|
some additional ones to test out various edge cases (and it's rewritten from pure unit-test suite).
|
|
|
|
On top of the original simple tests for the basic implementation, this suite also includes tests
|
|
with the 'bonus' marker, which cover the bonus task. (The basic implementation tests have the 'basic'
|
|
marker).
|
|
|
|
Finally, this suite also contains some extra cases for sibling matching (+ and ~). This is a part of
|
|
the CSS specification, however, it was not a part of even the bonus task. These tests are marked with
|
|
the 'extra' marker.
|
|
|
|
If you'd like to port this test-suite to your code, you'll want to change the src.qualifier imports to
|
|
just qualifier as that's what the original implementation expects.
|
|
"""
|
|
|
|
import unittest
|
|
from collections.abc import Iterable
|
|
|
|
import pytest
|
|
|
|
from src.node import Node
|
|
from src.qualifier import query_selector_all as solution
|
|
|
|
# This test tree matches the one from python discord's original test suite
|
|
#
|
|
# Structure:
|
|
# <div id="topDiv">
|
|
# <div class="container colour-primary" id="innerDiv">
|
|
# <h1>This is a heading!</h1>
|
|
# <p class="colour-secondary" id="innerContent">
|
|
# I have some content within this container also!
|
|
# </p>
|
|
# <p class="colour-secondary" id="two">
|
|
# This is another paragraph.
|
|
# </p>
|
|
# <p class="colour-secondary important">
|
|
# This is a third paragraph.
|
|
# </p>
|
|
# <a class="colour-primary button" id="home-link">
|
|
# This is a button link.
|
|
# </a>
|
|
# </div>
|
|
# <div class="container colour-secondary">
|
|
# <p class="colour-primary">This is a paragraph in a secondary container.</p>
|
|
# </div>
|
|
# </div>
|
|
TEST_TREE = Node(
|
|
tag="div",
|
|
attributes={"id": "topDiv"},
|
|
children=[
|
|
Node(
|
|
tag="div",
|
|
attributes={"id": "innerDiv", "class": "container colour-primary"},
|
|
children=[
|
|
Node(tag="h1", text="This is a heading!"),
|
|
Node(
|
|
tag="p",
|
|
attributes={"class": "colour-secondary", "id": "innerContent"},
|
|
text="I have some content within this container also!",
|
|
),
|
|
Node(
|
|
tag="p",
|
|
attributes={"class": "colour-secondary", "id": "two"},
|
|
text="This is another paragraph.",
|
|
),
|
|
Node(
|
|
tag="p",
|
|
attributes={"class": "colour-secondary important"},
|
|
text="This is a third paragraph.",
|
|
),
|
|
Node(
|
|
tag="a",
|
|
attributes={"id": "home-link", "class": "colour-primary button"},
|
|
text="This is a button link.",
|
|
),
|
|
],
|
|
),
|
|
Node(
|
|
tag="div",
|
|
attributes={"class": "container colour-secondary"},
|
|
children=[
|
|
Node(
|
|
tag="p",
|
|
attributes={"class": "colour-primary"},
|
|
text="This is a paragraph in a secondary container.",
|
|
),
|
|
],
|
|
),
|
|
],
|
|
)
|
|
|
|
# Get references to important nodes
|
|
TOP_DIV = TEST_TREE
|
|
INNER_DIV = TOP_DIV.children[0]
|
|
H1 = INNER_DIV.children[0]
|
|
P1 = INNER_DIV.children[1]
|
|
P2 = INNER_DIV.children[2]
|
|
P3 = INNER_DIV.children[3]
|
|
A_LINK = INNER_DIV.children[4]
|
|
SECOND_DIV = TEST_TREE.children[1]
|
|
SECOND_P = SECOND_DIV.children[0]
|
|
|
|
|
|
def assert_count_equal[T](first: Iterable[T], second: Iterable[T]) -> None:
|
|
case = unittest.TestCase()
|
|
case.assertCountEqual(first, second) # noqa: PT009
|
|
|
|
|
|
@pytest.mark.basic
|
|
@pytest.mark.parametrize(
|
|
("selector", "expected"),
|
|
[
|
|
# matches pydis tests exactly
|
|
("div", [TOP_DIV, INNER_DIV, SECOND_DIV]),
|
|
("#innerDiv", [INNER_DIV]),
|
|
(".colour-primary", [INNER_DIV, A_LINK, SECOND_P]),
|
|
("p.colour-secondary", [P1, P2, P3]),
|
|
("div#innerDiv", [INNER_DIV]),
|
|
("#innerContent.colour-secondary", [P1]),
|
|
("div#innerDiv.colour-primary", [INNER_DIV]),
|
|
(".colour-primary.button", [A_LINK]),
|
|
("#home-link.colour-primary.button", [A_LINK]),
|
|
("a#home-link.colour-primary.button", [A_LINK]),
|
|
("i", []),
|
|
("#badId", []),
|
|
(".missing-class", []),
|
|
# some extra tests
|
|
("#home-link", [A_LINK]),
|
|
("a.button#home-link", [A_LINK]),
|
|
("p.important", [P3]),
|
|
("p.nonexistent", []),
|
|
("p#innerContent", [P1]),
|
|
("h1#innerContent", []),
|
|
],
|
|
)
|
|
def test_simple_selector(selector: str, expected: list[Node]) -> None:
|
|
assert_count_equal(solution(TEST_TREE, selector), expected)
|
|
|
|
|
|
@pytest.mark.basic
|
|
@pytest.mark.parametrize(
|
|
("selector", "expected"),
|
|
[
|
|
# matches pydis tests exactly
|
|
("#topDiv, h1, .colour-primary", [TOP_DIV, H1, INNER_DIV, A_LINK, SECOND_P]),
|
|
("h1, a#home-link.colour-primary.button", [H1, A_LINK]),
|
|
("p#two.colour-secondary, a#home-link.colour-primary.button", [P2, A_LINK]),
|
|
("i, #badID, .missing-class", []),
|
|
("h1, #badID, .missing-class", [H1]),
|
|
("li#random.someclass, a#home-link, div, .colour-primary.badclass", [A_LINK, TOP_DIV, INNER_DIV, SECOND_DIV]),
|
|
],
|
|
)
|
|
def test_multi_selector(selector: str, expected: list[Node]) -> None:
|
|
assert_count_equal(solution(TEST_TREE, selector), expected)
|
|
|
|
|
|
@pytest.mark.bonus
|
|
@pytest.mark.parametrize(
|
|
("selector", "expected"),
|
|
[
|
|
("div.container > h1", [H1]),
|
|
("div > div > p.important", [P3]),
|
|
("div.container p.colour-secondary", [P1, P2, P3]),
|
|
("div.container > p.colour-secondary", [P1, P2, P3]),
|
|
("div#topDiv p.colour-secondary", [P1, P2, P3]),
|
|
("div#topDiv p", [P1, P2, P3, SECOND_P]),
|
|
("div#topDiv > p", []),
|
|
("div#innerDiv p.colour-primary", []),
|
|
("div.nonexistent p.colour-secondary", []),
|
|
("div#topDiv div#topDiv", []),
|
|
("div#topDiv > div.container > a", [A_LINK]),
|
|
],
|
|
)
|
|
def test_descendant_selector(selector: str, expected: list[Node]) -> None:
|
|
assert_count_equal(solution(TEST_TREE, selector), expected)
|
|
|
|
|
|
@pytest.mark.extra
|
|
@pytest.mark.parametrize(
|
|
("selector", "expected"),
|
|
[
|
|
("p.important + a.button", [A_LINK]),
|
|
("p.colour-secondary ~ a.button", [A_LINK]),
|
|
("p + p", [P2, P3]),
|
|
("p ~ p", [P2, P3]),
|
|
("h1 + p", [P1]),
|
|
("h1 ~ p", [P1, P2, P3]),
|
|
("div + h1", []),
|
|
("p.nonexistent + p", []),
|
|
("p + h1", []),
|
|
("p ~ h1", []),
|
|
("div + div", [SECOND_DIV]),
|
|
("div ~ div", [SECOND_DIV]),
|
|
("div#innerDiv ~ div", [SECOND_DIV]),
|
|
("div#topDiv ~ div", []),
|
|
],
|
|
)
|
|
def test_sibling_selector(selector: str, expected: list[Node]) -> None:
|
|
assert_count_equal(solution(TEST_TREE, selector), expected)
|
|
|
|
|
|
@pytest.mark.bonus
|
|
@pytest.mark.parametrize(
|
|
("selector", "expected"),
|
|
[
|
|
(":first-child", [INNER_DIV, H1, SECOND_P]),
|
|
(":last-child", [SECOND_DIV, A_LINK, SECOND_P]),
|
|
(":nth-child(1)", [INNER_DIV, H1, SECOND_P]),
|
|
(":nth-child(2)", [SECOND_DIV, P1]),
|
|
(":nth-child(3)", [P2]),
|
|
(":nth-child(4)", [P3]),
|
|
(":nth-child(5)", [A_LINK]),
|
|
(":nth-child(6)", []),
|
|
("p:first-child", [SECOND_P]),
|
|
("p:last-child", [SECOND_P]),
|
|
("p:nth-child(1)", [SECOND_P]),
|
|
("a:first-child", []),
|
|
("a:last-child", [A_LINK]),
|
|
("a:nth-child(5)", [A_LINK]),
|
|
("div:last-child", [SECOND_DIV]),
|
|
("div.nonexistent:last-child", []),
|
|
(".colour-primary:first-child", [INNER_DIV, SECOND_P]),
|
|
],
|
|
)
|
|
def test_nth_child_selector(selector: str, expected: list[Node]) -> None:
|
|
assert_count_equal(solution(TEST_TREE, selector), expected)
|
|
|
|
|
|
@pytest.mark.bonus
|
|
@pytest.mark.parametrize(
|
|
("selector", "expected"),
|
|
[
|
|
(":not(p)", [TOP_DIV, INNER_DIV, H1, A_LINK, SECOND_DIV]),
|
|
("p:not(.important)", [P1, P2, SECOND_P]),
|
|
("div:not(.important)", [TOP_DIV, INNER_DIV, SECOND_DIV]),
|
|
(":not(.important)", [TOP_DIV, INNER_DIV, H1, P1, P2, A_LINK, SECOND_DIV, SECOND_P]),
|
|
("div:not(#innerDiv)", [TOP_DIV, SECOND_DIV]),
|
|
("#innerDiv:not(p)", [INNER_DIV]),
|
|
("#innerDiv:not(div)", []),
|
|
("p:not(p)", []),
|
|
(":not(div):not(p):not(a)", [H1]),
|
|
(":not(div):not(p):not(a):not(h1)", []),
|
|
("p:not(.colour-primary:not(#two))", [P1, P2, P3]),
|
|
("p:not(.colour-secondary:not(#two))", [P2, SECOND_P]),
|
|
(":not(:not(div))", [TOP_DIV, INNER_DIV, SECOND_DIV]),
|
|
],
|
|
)
|
|
def test_not_selector(selector: str, expected: list[Node]) -> None:
|
|
assert_count_equal(solution(TEST_TREE, selector), expected)
|
|
|
|
|
|
@pytest.mark.basic
|
|
@pytest.mark.parametrize(
|
|
("selector", "expected"),
|
|
[
|
|
("q, b", []),
|
|
],
|
|
)
|
|
def test_combined_basic(selector: str, expected: list[Node]) -> None:
|
|
assert_count_equal(solution(TEST_TREE, selector), expected)
|
|
|
|
|
|
@pytest.mark.bonus
|
|
@pytest.mark.parametrize(
|
|
("selector", "expected"),
|
|
[
|
|
("p:first-child:not(:last-child)", []),
|
|
("h1:first-child:not(:last-child)", [H1]),
|
|
(":first-child:not(:nth-child(2))", [INNER_DIV, SECOND_P, H1]),
|
|
(":first-child:not(:nth-child(2)):not(h1)", [INNER_DIV, SECOND_P]),
|
|
(":first-child:not(:nth-child(2), h1)", [INNER_DIV, SECOND_P]),
|
|
("p:not(p:first-child)", [P1, P2, P3]),
|
|
("p:not(p:nth-child(1))", [P1, P2, P3]),
|
|
("p:not(.colour-secondary:not(#two)):not(:first-child)", [P2]),
|
|
("p:not(div.colour-secondary p)", [P1, P2, P3]),
|
|
(":not(div, p, a)", [H1]),
|
|
(":not(div, p, a, h1)", []),
|
|
(":not(div p)", [TOP_DIV, INNER_DIV, H1, A_LINK, SECOND_DIV]),
|
|
(".colour-primary > p:not(#two)", [P1, P3]),
|
|
(".colour-primary p:not(#two)", [P1, P3]),
|
|
("#topDiv p:not(#two)", [P1, P3, SECOND_P]),
|
|
("#topDiv > p:not(#two)", []),
|
|
],
|
|
)
|
|
def test_combined_bonus(selector: str, expected: list[Node]) -> None:
|
|
assert_count_equal(solution(TEST_TREE, selector), expected)
|
|
|
|
|
|
@pytest.mark.basic
|
|
@pytest.mark.parametrize(
|
|
("selector", "expected"),
|
|
[
|
|
("a,div", [A_LINK, TOP_DIV, INNER_DIV, SECOND_DIV]),
|
|
("a ,div", [A_LINK, TOP_DIV, INNER_DIV, SECOND_DIV]),
|
|
("a, div", [A_LINK, TOP_DIV, INNER_DIV, SECOND_DIV]),
|
|
("a , div", [A_LINK, TOP_DIV, INNER_DIV, SECOND_DIV]),
|
|
("a , div", [A_LINK, TOP_DIV, INNER_DIV, SECOND_DIV]),
|
|
("a#home-link , div", [A_LINK, TOP_DIV, INNER_DIV, SECOND_DIV]),
|
|
("a.button#home-link , div.container", [A_LINK, INNER_DIV, SECOND_DIV]),
|
|
],
|
|
)
|
|
def test_whitespace_basic(selector: str, expected: list[Node]) -> None:
|
|
assert_count_equal(solution(TEST_TREE, selector), expected)
|
|
|
|
|
|
@pytest.mark.bonus
|
|
@pytest.mark.parametrize(
|
|
("selector", "expected"),
|
|
[
|
|
("div.container>h1", [H1]),
|
|
("div.container> h1", [H1]),
|
|
("div.container >h1", [H1]),
|
|
("div.container > h1", [H1]),
|
|
("div.container > h1", [H1]),
|
|
("div#topDiv >div> a", [A_LINK]),
|
|
("div#topDiv >div a", [A_LINK]),
|
|
("div#topDiv >div:not(.colour-secondary) a", [A_LINK]),
|
|
("div:not(.colour-secondary)", [TOP_DIV, INNER_DIV]),
|
|
("div:not( .colour-secondary)", [TOP_DIV, INNER_DIV]),
|
|
("div:not(.colour-secondary )", [TOP_DIV, INNER_DIV]),
|
|
("div:not( .colour-secondary )", [TOP_DIV, INNER_DIV]),
|
|
("div:not( .colour-secondary )", [TOP_DIV, INNER_DIV]),
|
|
("div#topDiv >div:not( .colour-secondary ) a", [A_LINK]),
|
|
],
|
|
)
|
|
def test_whitespace_bonus(selector: str, expected: list[Node]) -> None:
|
|
assert_count_equal(solution(TEST_TREE, selector), expected)
|