fix: update exam test logic to handle CSRF token extraction and unanswered question counts
This commit is contained in:
@@ -140,10 +140,36 @@ def test_exams(db, client):
|
|||||||
|
|
||||||
cid_take_soup = BeautifulSoup(cid_take_res.content, "html.parser")
|
cid_take_soup = BeautifulSoup(cid_take_res.content, "html.parser")
|
||||||
|
|
||||||
# Extract csrftoken
|
# The question form controls (including CSRF token) are loaded via
|
||||||
csrftoken = cid_take_soup.find(
|
# the HTMX fragment. Request that fragment and parse it for the
|
||||||
"input", attrs={"name": "csrfmiddlewaretoken"}
|
# form elements instead of relying on the main container page.
|
||||||
)["value"]
|
if exam_tester.testing_cid_user:
|
||||||
|
frag_url = reverse(
|
||||||
|
f"{APP_NAME}:exam_take_fragment",
|
||||||
|
kwargs={
|
||||||
|
"pk": exam.pk,
|
||||||
|
"sk": n,
|
||||||
|
"cid": exam_tester.current_user.cid,
|
||||||
|
"passcode": exam_tester.current_user.passcode,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
frag_url = reverse(
|
||||||
|
f"{APP_NAME}:exam_take_fragment_user",
|
||||||
|
kwargs={
|
||||||
|
"pk": exam.pk,
|
||||||
|
"sk": n,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
frag_res = client.get(frag_url)
|
||||||
|
assert frag_res.status_code == HTTPStatus.OK
|
||||||
|
frag_soup = BeautifulSoup(frag_res.content, "html.parser")
|
||||||
|
|
||||||
|
# Extract csrftoken from the fragment
|
||||||
|
csrftoken_el = frag_soup.find("input", attrs={"name": "csrfmiddlewaretoken"})
|
||||||
|
assert csrftoken_el is not None
|
||||||
|
csrftoken = csrftoken_el["value"]
|
||||||
# Send answers (all TRUE)
|
# Send answers (all TRUE)
|
||||||
post_json = {
|
post_json = {
|
||||||
"csrfmiddlewaretoken": csrftoken,
|
"csrfmiddlewaretoken": csrftoken,
|
||||||
@@ -156,7 +182,7 @@ def test_exams(db, client):
|
|||||||
}
|
}
|
||||||
res = client.post(question_url, data=post_json)
|
res = client.post(question_url, data=post_json)
|
||||||
|
|
||||||
next_button = cid_take_soup.find("button", attrs={"name": "next"})
|
next_button = frag_soup.find("button", attrs={"name": "next"})
|
||||||
|
|
||||||
# Our final question will trigger a bad request (it shouldn't show a next button)
|
# Our final question will trigger a bad request (it shouldn't show a next button)
|
||||||
# We currently do still save the form though....
|
# We currently do still save the form though....
|
||||||
@@ -167,10 +193,10 @@ def test_exams(db, client):
|
|||||||
assert next_button
|
assert next_button
|
||||||
assert res.status_code == HTTPStatus.FOUND
|
assert res.status_code == HTTPStatus.FOUND
|
||||||
|
|
||||||
overview_button = cid_take_soup.find("button", attrs={"name": "finish"})
|
overview_button = frag_soup.find("button", attrs={"name": "finish"})
|
||||||
assert overview_button
|
assert overview_button
|
||||||
|
|
||||||
previous_button = cid_take_soup.find("button", attrs={"name": "previous"})
|
previous_button = frag_soup.find("button", attrs={"name": "previous"})
|
||||||
|
|
||||||
if n > 0:
|
if n > 0:
|
||||||
assert previous_button
|
assert previous_button
|
||||||
|
|||||||
+34
-16
@@ -327,14 +327,23 @@ class ExamTester:
|
|||||||
).text
|
).text
|
||||||
)
|
)
|
||||||
|
|
||||||
assert (
|
# Different apps render unanswered markers differently:
|
||||||
len(
|
# - `sbas` uses <button class="unanswered"> nodes
|
||||||
|
# - `physics` uses anchor tiles with a title attribute for unanswered
|
||||||
|
if self.app_name == "sbas":
|
||||||
|
count_unanswered = len(
|
||||||
cid_take_overview_soup.find_all(
|
cid_take_overview_soup.find_all(
|
||||||
"button", attrs={"class": "unanswered"}
|
"button", attrs={"class": "unanswered"}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
== total - answered
|
else:
|
||||||
)
|
# Count overview tiles that include the "not answered" title
|
||||||
|
overview_tiles = cid_take_overview_soup.find_all(
|
||||||
|
class_="overview-question-btn"
|
||||||
|
)
|
||||||
|
count_unanswered = sum(1 for t in overview_tiles if t.has_attr("title"))
|
||||||
|
|
||||||
|
assert count_unanswered == total - answered
|
||||||
|
|
||||||
def check_cid_scores_page(self):
|
def check_cid_scores_page(self):
|
||||||
if self.testing_cid_user:
|
if self.testing_cid_user:
|
||||||
@@ -457,17 +466,20 @@ class ExamTester:
|
|||||||
alert = cid_exam_scores_soup.find("div", {"class": "alert"})
|
alert = cid_exam_scores_soup.find("div", {"class": "alert"})
|
||||||
assert "Results are not currently published." in str(alert)
|
assert "Results are not currently published." in str(alert)
|
||||||
|
|
||||||
|
# Find any element with the submitted-user-answer class (div or span)
|
||||||
submitted_answers = cid_exam_scores_soup.find_all(
|
submitted_answers = cid_exam_scores_soup.find_all(
|
||||||
"span", {"class": "submitted-user-answer"}
|
class_="submitted-user-answer"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check that we are showing enough questions / answers
|
# Check that we are showing enough questions / answers
|
||||||
match self.app_name:
|
match self.app_name:
|
||||||
case "physics":
|
case "physics":
|
||||||
answer_lis = cid_exam_scores_soup.find_all(
|
# Physics renders per-part answers inside an ordered list
|
||||||
"li", {"class": "question-part"}
|
# within each question block. Count li elements inside those
|
||||||
)
|
# lists to get the total number of parts shown.
|
||||||
assert len(answer_lis) == 5 * len(self.generated_questions)
|
ols = cid_exam_scores_soup.find_all("ol", class_="mb-0")
|
||||||
|
answer_li_count = sum(len(ol.find_all("li")) for ol in ols)
|
||||||
|
assert answer_li_count == 5 * len(self.generated_questions)
|
||||||
assert len(submitted_answers) == 5 * len(self.generated_questions)
|
assert len(submitted_answers) == 5 * len(self.generated_questions)
|
||||||
for ans in submitted_answers:
|
for ans in submitted_answers:
|
||||||
assert ans.text.strip() == "True"
|
assert ans.text.strip() == "True"
|
||||||
@@ -497,10 +509,9 @@ class ExamTester:
|
|||||||
)
|
)
|
||||||
match self.app_name:
|
match self.app_name:
|
||||||
case "physics":
|
case "physics":
|
||||||
answer_lis = cid_exam_scores_soup.find_all(
|
ols = cid_exam_scores_soup.find_all("ol", class_="mb-0")
|
||||||
"li", {"class": "question-part"}
|
answer_li_count = sum(len(ol.find_all("li")) for ol in ols)
|
||||||
)
|
assert answer_li_count == 5 * len(self.generated_questions)
|
||||||
assert len(answer_lis) == 5 * len(self.generated_questions)
|
|
||||||
assert len(submitted_answers) == 5 * len(self.generated_questions)
|
assert len(submitted_answers) == 5 * len(self.generated_questions)
|
||||||
|
|
||||||
for ans in submitted_answers[:-5]:
|
for ans in submitted_answers[:-5]:
|
||||||
@@ -541,9 +552,16 @@ class ExamTester:
|
|||||||
) # Check our warning banner is gone
|
) # Check our warning banner is gone
|
||||||
|
|
||||||
# Check that we are displaying the correct answers (correctly)
|
# Check that we are displaying the correct answers (correctly)
|
||||||
correct_answers = cid_exam_scores_soup.find_all(
|
# Different apps render correct answers differently. For physics the
|
||||||
"span", {"class": "correct-answer"}
|
# template uses a div containing the text "Correct answer: ...".
|
||||||
)
|
if self.app_name == "physics":
|
||||||
|
correct_answers = [
|
||||||
|
t for t in cid_exam_scores_soup.find_all(string=True) if "Correct answer:" in t
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
correct_answers = cid_exam_scores_soup.find_all(
|
||||||
|
"span", {"class": "correct-answer"}
|
||||||
|
)
|
||||||
|
|
||||||
assert len(correct_answers) == self.answers_per_question * len(
|
assert len(correct_answers) == self.answers_per_question * len(
|
||||||
self.generated_questions
|
self.generated_questions
|
||||||
|
|||||||
Reference in New Issue
Block a user