"""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: #
#
#

This is a heading!

#

# I have some content within this container also! #

#

# This is another paragraph. #

#

# This is a third paragraph. #

# # This is a button link. # #
#
#

This is a paragraph in a secondary container.

#
#
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]), (":first-child.colour-primary", [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)