142 lines
4.9 KiB
Python
142 lines
4.9 KiB
Python
import re
|
|
import unittest
|
|
from html.parser import HTMLParser
|
|
from pathlib import Path
|
|
|
|
|
|
WEB_ROOT = Path("src/web")
|
|
|
|
|
|
def _read_web_file(name: str) -> str:
|
|
return (WEB_ROOT / name).read_text(encoding="utf-8")
|
|
|
|
|
|
class _MarkupParser(HTMLParser):
|
|
def __init__(self) -> None:
|
|
super().__init__()
|
|
self.elements: list[tuple[str, dict[str, str]]] = []
|
|
self.buttons_by_id: dict[str, dict[str, str]] = {}
|
|
|
|
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
|
|
attr_map = {key: value or "" for key, value in attrs}
|
|
self.elements.append((tag, attr_map))
|
|
if tag == "button" and attr_map.get("id"):
|
|
self.buttons_by_id[attr_map["id"]] = attr_map
|
|
|
|
|
|
def _parse_training_html() -> _MarkupParser:
|
|
parser = _MarkupParser()
|
|
parser.feed(_read_web_file("training.html"))
|
|
return parser
|
|
|
|
|
|
class TrainingDashboardM3Tests(unittest.TestCase):
|
|
def test_training_shell_is_moved_to_dedicated_page(self) -> None:
|
|
index_html = _read_web_file("index.html")
|
|
training_html = _read_web_file("training.html")
|
|
|
|
self.assertNotIn('id="trainingSection"', index_html)
|
|
for expected in (
|
|
'id="trainingSection"',
|
|
'id="trainingCasesList"',
|
|
'id="trainingCaseDetail"',
|
|
'id="trainingProgress"',
|
|
'id="trainingAttemptStatus"',
|
|
'id="trainingImportText"',
|
|
'id="trainingAnnotationText"',
|
|
'/static/training.js',
|
|
):
|
|
self.assertIn(expected, training_html)
|
|
|
|
def test_training_page_keeps_public_workflow_sections(self) -> None:
|
|
training_html = _read_web_file("training.html")
|
|
styles_css = _read_web_file("styles.css")
|
|
parser = _parse_training_html()
|
|
attrs_by_id = {attrs["id"]: attrs for _tag, attrs in parser.elements if attrs.get("id")}
|
|
|
|
for expected in (
|
|
"Training workflow",
|
|
"Import / Export",
|
|
"Cases",
|
|
"Case Detail",
|
|
"Progress",
|
|
"Only public training context is shown.",
|
|
):
|
|
self.assertIn(expected, training_html)
|
|
|
|
for expected in (
|
|
".training-grid",
|
|
".training-toolbar",
|
|
".training-case-list",
|
|
".training-detail",
|
|
".training-actions",
|
|
".training-error",
|
|
):
|
|
self.assertIn(expected, styles_css)
|
|
|
|
self.assertEqual(attrs_by_id["trainingError"].get("role"), "alert")
|
|
self.assertEqual(attrs_by_id["trainingCasesList"].get("role"), "list")
|
|
self.assertEqual(attrs_by_id["trainingProgress"].get("role"), "list")
|
|
self.assertEqual(parser.buttons_by_id["trainingImportBtn"].get("type"), "button")
|
|
self.assertEqual(parser.buttons_by_id["trainingSubmitBtn"].get("type"), "button")
|
|
|
|
def test_training_app_uses_public_route_registry(self) -> None:
|
|
training_js = _read_web_file("training.js")
|
|
|
|
for expected in (
|
|
"TRAINING_PUBLIC_ROUTES",
|
|
"list: '/training/cases'",
|
|
"detail: (caseId) => `/training/cases/${caseId}`",
|
|
"export: '/training/cases/export'",
|
|
"import: '/training/cases/import'",
|
|
"createAttempt: '/training/attempts'",
|
|
"annotateAttempt: (attemptId) => `/training/attempts/${attemptId}/annotations`",
|
|
"submitAttempt: (attemptId) => `/training/attempts/${attemptId}/submit`",
|
|
"progress: '/training/progress'",
|
|
"loadTrainingDashboard",
|
|
"loadTrainingCaseDetail",
|
|
"startTrainingAttempt",
|
|
"saveTrainingAnnotation",
|
|
"submitTrainingAttempt",
|
|
"refreshTrainingProgress",
|
|
):
|
|
self.assertIn(expected, training_js)
|
|
|
|
self.assertRegex(training_js, re.compile(r"method:\s*'POST'"))
|
|
self.assertIn("annotation_type: 'note'", training_js)
|
|
self.assertIn("body: JSON.stringify({})", training_js)
|
|
|
|
def test_training_frontend_keeps_private_material_out(self) -> None:
|
|
web_source = "\n".join(
|
|
_read_web_file(name).lower()
|
|
for name in ("training.html", "training.js", "styles.css")
|
|
)
|
|
for forbidden in (
|
|
"answer_key",
|
|
"solution_key",
|
|
"internal solution details",
|
|
"private solution material",
|
|
"postgresql",
|
|
"password",
|
|
"secret",
|
|
"api_key",
|
|
):
|
|
self.assertNotIn(forbidden, web_source)
|
|
|
|
def test_training_optional_controls_are_guarded(self) -> None:
|
|
training_js = _read_web_file("training.js")
|
|
|
|
for expected in (
|
|
"if (trainingSaveAnnotationBtn)",
|
|
"if (trainingSubmitBtn)",
|
|
"if (trainingImportBtn)",
|
|
"if (trainingExportBtn)",
|
|
"if (trainingReloadBtn)",
|
|
"if (trainingRefreshProgressBtn)",
|
|
):
|
|
self.assertIn(expected, training_js)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|