Skip to content

Commit

Permalink
Create html & css check decorators
Browse files Browse the repository at this point in the history
  • Loading branch information
stijndcl committed Oct 23, 2021
1 parent 2296fb1 commit 3b00570
Show file tree
Hide file tree
Showing 4 changed files with 63 additions and 34 deletions.
1 change: 1 addition & 0 deletions decorators/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from .check_wrappers import html_check, css_check
from .flatten import flatten_varargs
34 changes: 34 additions & 0 deletions decorators/check_wrappers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from bs4 import BeautifulSoup


def fail():
"""Return a Check that always fails"""
# Local import to avoid circular dependencies
from validators.checks import Check

def _inner(_: BeautifulSoup) -> bool:
return False

return Check(_inner)


def html_check(func):
"""Decorator that checks if an HTML element is not None"""
def wrapper(*args, **kwargs):
if args[0]._element is None:
return fail()

return func(*args, **kwargs)

return wrapper


def css_check(func):
"""Decorator that checks if an element's HTML tag and CSS validator are not None"""
def wrapper(*args, **kwargs):
if args[0]._element is None or args[0]._css_validator is None:
return fail()

return func(*args, **kwargs)

return wrapper
60 changes: 26 additions & 34 deletions validators/checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from bs4 import BeautifulSoup
from bs4.element import Tag, NavigableString

from decorators import flatten_varargs
from decorators import flatten_varargs, html_check, css_check
from dodona.dodona_command import Context, TestCase, Message, MessageFormat, Annotation
from dodona.dodona_config import DodonaConfig
from dodona.translator import Translator
Expand Down Expand Up @@ -160,6 +160,7 @@ def _inner(_: BeautifulSoup) -> bool:

return Check(_inner)

@html_check
def has_child(self, tag: Optional[Union[str, Emmet]] = None, direct: bool = True, **kwargs) -> Check:
"""Check that this element has a child with the given tag
Expand All @@ -173,6 +174,7 @@ def _inner(_: BeautifulSoup) -> bool:

return Check(_inner)

@html_check
def has_content(self, text: Optional[str] = None, case_insensitive: bool = False) -> Check:
"""Check if this element has given text as content.
In case no text is passed, any non-empty string will make the test pass
Expand All @@ -189,12 +191,9 @@ def has_content(self, text: Optional[str] = None, case_insensitive: bool = False
"""

def _inner(_: BeautifulSoup) -> bool:
# Element doesn't exist
if self._element is None:
return False

# No text in this element
if self._element.text is None:
return text is None
return False

if text is not None:
return compare_content(self._element.text, text, case_insensitive)
Expand All @@ -205,8 +204,9 @@ def _inner(_: BeautifulSoup) -> bool:

def _has_tag(self, tag: str) -> bool:
"""Internal function that checks if this element has the required tag"""
return self._element is not None and self._element.name.lower() == tag.lower()
return self._element.name.lower() == tag.lower()

@html_check
def has_tag(self, tag: str) -> Check:
"""Check that this element has the required tag"""

Expand All @@ -215,17 +215,11 @@ def _inner(_: BeautifulSoup) -> bool:

return Check(_inner)

@html_check
def no_loose_text(self) -> Check:
"""Check that there is no content floating around in this tag"""

def _inner(_: BeautifulSoup) -> bool:
# Even though a non-existent element has no text,
# so it may seem as this should always pass,
# the standard behaviour is that Checks for these elements
# should always fail
if self._element is None:
return False

children = self._element.children

for child in children:
Expand All @@ -241,9 +235,6 @@ def _inner(_: BeautifulSoup) -> bool:

def _get_attribute(self, attr: str) -> Optional[Union[List[str], str]]:
"""Internal function that gets an attribute"""
if self._element is None:
return None

attribute = self._element.get(attr.lower())

return attribute
Expand Down Expand Up @@ -287,6 +278,7 @@ def _compare_attribute_list(self, attribute: List[str], value: Optional[str] = N
# Possible future modes
return False

@html_check
def attribute_exists(self, attr: str, value: Optional[str] = None, case_insensitive: bool = False) -> Check:
"""Check that this element has the required attribute, optionally with a value
:param attr: The name of the attribute to check.
Expand Down Expand Up @@ -315,6 +307,7 @@ def _inner(_: BeautifulSoup) -> bool:

return Check(_inner)

@html_check
def attribute_contains(self, attr: str, substr: str, case_insensitive: bool = False) -> Check:
"""Check that the value of this attribute contains a substring"""

Expand All @@ -335,6 +328,7 @@ def _inner(_: BeautifulSoup) -> bool:

return Check(_inner)

@html_check
def attribute_matches(self, attr: str, regex: str, flags: Union[int, re.RegexFlag] = 0) -> Check:
"""Check that the value of an attribute matches a regex pattern"""

Expand All @@ -352,11 +346,12 @@ def _inner(_: BeautifulSoup) -> bool:

return Check(_inner)

@html_check
def has_table_header(self, header: List[str]) -> Check:
"""If this element is a table, check that the header content matches up"""

def _inner(_: BeautifulSoup) -> bool:
# This element is either None or not a table
# This element is not a table
if not self._has_tag("table"):
return False

Expand All @@ -376,6 +371,7 @@ def _inner(_: BeautifulSoup) -> bool:

return Check(_inner)

@html_check
def has_table_content(self, rows: List[List[str]], has_header: bool = True, case_insensitive: bool = False) -> Check:
"""Check that a table's rows have the requested content
:param rows: The data of all the rows to check
Expand All @@ -385,7 +381,7 @@ def has_table_content(self, rows: List[List[str]], has_header: bool = True, case
"""

def _inner(_: BeautifulSoup) -> bool:
# This element is either None or not a table
# This element is not a table
if not self._has_tag("table"):
return False

Expand Down Expand Up @@ -425,11 +421,12 @@ def _inner(_: BeautifulSoup) -> bool:

return Check(_inner)

@html_check
def table_row_has_content(self, row: List[str], case_insensitive: bool = False) -> Check:
"""Check the content of one row instead of the whole table"""

def _inner(_: BeautifulSoup) -> bool:
# Check that this element exists and is a <tr>
# Check that this element is a <tr>
if not self._has_tag("tr"):
return False

Expand All @@ -448,13 +445,14 @@ def _inner(_: BeautifulSoup) -> bool:

return Check(_inner)

@html_check
def has_url_with_fragment(self, fragment: Optional[str] = None) -> Check:
"""Check if a url has a fragment
If no fragment is passed, any non-empty fragment will do
"""

def _inner(_: BeautifulSoup) -> bool:
if self._element is None or self.tag.lower() != "a":
if not self._has_tag("a"):
return False

url = self._get_attribute("href")
Expand All @@ -477,6 +475,7 @@ def _inner(_: BeautifulSoup) -> bool:

return Check(_inner)

@html_check
def has_outgoing_url(self, allowed_domains: Optional[List[str]] = None, attr: str = "href") -> Check:
"""Check if a tag has an outgoing link
:param allowed_domains: A list of domains that should not be considered "outgoing",
Expand All @@ -487,9 +486,6 @@ def has_outgoing_url(self, allowed_domains: Optional[List[str]] = None, attr: st
allowed_domains = ["dodona.ugent.be", "users.ugent.be"]

def _inner(_: BeautifulSoup) -> bool:
if self._element is None:
return False

url = self._get_attribute(attr.lower())

# No url present
Expand All @@ -507,6 +503,7 @@ def _inner(_: BeautifulSoup) -> bool:

return Check(_inner)

@html_check
def contains_comment(self, comment: Optional[str] = None) -> Check:
"""Check if the element contains a comment, optionally matching a value"""

Expand All @@ -516,6 +513,7 @@ def _inner(_: BeautifulSoup) -> bool:
return Check(_inner)

# CSS checks
@css_check
def has_styling(self, prop: str, value: Optional[str] = None, important: Optional[bool] = None) -> Check:
"""Check that this element has a CSS property
:param prop: the required CSS property to check
Expand All @@ -525,13 +523,6 @@ def has_styling(self, prop: str, value: Optional[str] = None, important: Optiona
"""

def _inner(_: BeautifulSoup) -> bool:
if self._element is None:
return False

# This shouldn't happen if the element exists, but just in case
if self._css_validator is None:
return False

prop_value = self._css_validator.find(self._element, prop.lower())

# Property not found
Expand All @@ -550,6 +541,7 @@ def _inner(_: BeautifulSoup) -> bool:

return Check(_inner)

@css_check
def has_color(self, prop: str, color: str, important: Optional[bool] = None) -> Check:
"""Check that this element has a given color
More flexible version of has_styling because it also allows RGB(r, g, b), hex format, ...
Expand All @@ -560,9 +552,6 @@ def has_color(self, prop: str, color: str, important: Optional[bool] = None) ->
"""

def _inner(_: BeautifulSoup) -> bool:
if self._element is None or self._css_validator is None:
return False

# Find the CSS Rule
prop_rule = self._css_validator.find(self._element, prop.lower())

Expand All @@ -582,6 +571,9 @@ class EmptyElement(Element):
def __init__(self):
super().__init__("", None, None, None)

def __str__(self) -> str:
return "<EmptyElement>"


@dataclass
class ElementContainer:
Expand Down
2 changes: 2 additions & 0 deletions validators/checks.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ class Element:
class EmptyElement(Element):
def __init__(self): ...

def __str__(self) -> str: ...


class ElementContainer:
elements: List[Element]
Expand Down

0 comments on commit 3b00570

Please sign in to comment.