Initial commit

This commit is contained in:
ItsDrike 2025-07-27 12:51:35 +02:00
commit 9fba4b3d34
Signed by: ItsDrike
GPG key ID: FA2745890B7048C0
17 changed files with 2939 additions and 0 deletions

335
tests/test_qualifier.py Normal file
View file

@ -0,0 +1,335 @@
"""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)