Quiiz system

This commit is contained in:
2026-07-26 16:46:41 +02:00
parent 7f17d69f5c
commit 4780016980
192 changed files with 15031 additions and 566 deletions
+33
View File
@@ -3,6 +3,8 @@
import re
from playwright.sync_api import expect
from tests.conftest import BASE_URL
UNICORN = "\U0001F984"
def _open_composer(page):
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
page.locator(".feed-fab").first.wait_for(state="visible", timeout=10000)
@@ -37,3 +39,34 @@ def test_reaction_palette_toggle_and_react(alice):
expect(
page.locator(".reaction-chip.reacted").first.locator(".reaction-count")
).to_have_text("1")
def test_reaction_with_an_emoji_outside_the_palette(alice):
page, _ = alice
_create_plain_post_engagement_ui(page, "Post to react to with an off-palette emoji.")
page.locator(".reaction-add-btn").first.click()
page.locator(".reaction-palette-more").first.click()
picker = page.locator(".reaction-picker").first
picker.wait_for(state="visible")
picker.locator("emoji-picker").wait_for(state="attached")
page.evaluate(
"""(unicode) => {
const picker = document.querySelector(".reaction-picker emoji-picker");
picker.dispatchEvent(new CustomEvent("emoji-click", { detail: { unicode } }));
}""",
UNICORN,
)
chip = page.locator(f'.reaction-chip[data-reaction-emoji="{UNICORN}"]')
expect(chip).to_be_visible()
expect(chip).to_have_class(re.compile("reacted"))
expect(chip.locator(".reaction-count")).to_have_text("1")
expect(picker).to_be_hidden()
page.reload(wait_until="domcontentloaded")
reloaded = page.locator(f'.reaction-chip[data-reaction-emoji="{UNICORN}"]')
expect(reloaded).to_be_visible()
expect(reloaded.locator(".reaction-count")).to_have_text("1")
+4 -2
View File
@@ -148,7 +148,7 @@ def test_owner_sees_harvest_not_steal(alice):
assert page.locator("form[data-game-action='steal']").count() == 0
def test_victim_gets_anonymous_notification(bob):
def test_victim_notification_names_the_raider_and_the_amount(bob):
page, _ = bob
from devplacepy.database import get_table
@@ -172,4 +172,6 @@ def test_victim_gets_anonymous_notification(bob):
user_uid=alice["uid"], type="harvest_stolen"
)
assert note is not None
assert "bob_test" not in note["message"]
assert "bob_test" in note["message"]
assert "Python Script" in note["message"]
assert "harvest it" in note["message"]
+27 -15
View File
@@ -75,8 +75,18 @@ def open_game(page):
page.wait_for_timeout(800)
def hud_number(value):
from devplacepy.templating import format_number
return format_number(value)
def coins(page):
return int(page.locator("[data-hud-coins]").inner_text())
node = page.locator("[data-hud-coins]")
exact = node.get_attribute("title")
if exact:
return int(exact.replace(",", ""))
return int(node.inner_text().replace(",", ""))
def test_game_page_loads(alice):
@@ -84,7 +94,7 @@ def test_game_page_loads(alice):
reset_farm("alice_test")
open_game(page)
assert page.locator(".game-header h1").is_visible()
expect(page.locator("[data-hud-coins]")).to_have_text("100000")
expect(page.locator("[data-hud-coins]")).to_have_text(hud_number(100000))
expect(page.locator("[data-game-grid] [data-slot]")).to_have_count(4)
expect(page.locator("[data-perk-host] .perk-card")).to_have_count(4)
expect(page.locator(".quest-item")).to_have_count(3)
@@ -104,7 +114,7 @@ def test_plant_crop(alice):
form.locator("select[name='crop']").select_option("shell")
form.locator("button").click()
expect(page.locator(".game-plot-growing")).to_have_count(1)
expect(page.locator("[data-hud-coins]")).to_have_text("99995")
expect(page.locator("[data-hud-coins]")).to_have_text(hud_number(99995))
def test_harvest_crop(alice):
@@ -118,7 +128,7 @@ def test_harvest_crop(alice):
warp_ready("alice_test")
open_game(page)
page.locator("form[data-game-action='harvest'] button").first.click()
expect(page.locator("[data-hud-coins]")).not_to_have_text("99995")
expect(page.locator("[data-hud-coins]")).not_to_have_text(hud_number(99995))
assert coins(page) > 99995
expect(page.locator(".game-plot-growing")).to_have_count(0)
@@ -130,11 +140,11 @@ def test_fertilize_spends_coins(alice):
form = page.locator("form[data-game-action='plant']").first
form.locator("select[name='crop']").select_option("python")
form.locator("button").click()
expect(page.locator("[data-hud-coins]")).to_have_text("99985")
expect(page.locator("[data-hud-coins]")).to_have_text(hud_number(99985))
fert = page.locator("form[data-game-action='fertilize'] button").first
fert.wait_for(state="visible")
fert.click()
expect(page.locator("[data-hud-coins]")).not_to_have_text("99985")
expect(page.locator("[data-hud-coins]")).not_to_have_text(hud_number(99985))
assert coins(page) < 99985
@@ -144,7 +154,7 @@ def test_buy_plot(alice):
open_game(page)
page.locator("form[data-game-action='buy-plot'] button").click()
expect(page.locator("[data-game-grid] [data-slot]")).to_have_count(5)
expect(page.locator("[data-hud-coins]")).to_have_text("99900")
expect(page.locator("[data-hud-coins]")).to_have_text(hud_number(99900))
def test_upgrade_ci(alice):
@@ -153,7 +163,7 @@ def test_upgrade_ci(alice):
open_game(page)
page.locator("form[data-game-action='upgrade'] button").click()
expect(page.locator("[data-hud-ci]")).to_have_text("2")
expect(page.locator("[data-hud-coins]")).to_have_text("99850")
expect(page.locator("[data-hud-coins]")).to_have_text(hud_number(99850))
def test_claim_daily(alice):
@@ -161,7 +171,7 @@ def test_claim_daily(alice):
reset_farm("alice_test")
open_game(page)
page.locator("form[data-game-action='daily'] button").click()
expect(page.locator("[data-hud-coins]")).to_have_text("100020")
expect(page.locator("[data-hud-coins]")).to_have_text(hud_number(100020))
expect(page.locator("[data-daily-host] .daily-claimed")).to_be_visible()
@@ -173,7 +183,7 @@ def test_upgrade_perk(alice):
"form[data-game-action='perk']:has(input[value='growth']) button"
)
growth.click()
expect(page.locator("[data-hud-coins]")).to_have_text("99850")
expect(page.locator("[data-hud-coins]")).to_have_text(hud_number(99850))
expect(
page.locator(".perk-card:has-text('Build Cache') .perk-level")
).to_have_text("Lv 1/10")
@@ -217,7 +227,7 @@ def test_claim_quest(alice):
)
open_game(page)
page.locator("form[data-game-action='claim-quest'] button").first.click()
expect(page.locator("[data-hud-coins]")).not_to_have_text("100000")
expect(page.locator("[data-hud-coins]")).not_to_have_text(hud_number(100000))
assert coins(page) > 100000
@@ -246,7 +256,7 @@ def test_prestige_resets_farm(alice):
confirm_btn.click()
expect(page.locator("[data-hud-prestige]")).to_have_text("1")
expect(page.locator("[data-hud-coins]")).to_have_text(
str(economy.STARTING_COINS + carried)
hud_number(economy.STARTING_COINS + carried)
)
assert page.locator("form[data-game-action='prestige']").count() == 0
@@ -283,7 +293,7 @@ def test_grant_section_claim(alice):
claim.wait_for(state="visible")
claim.click()
expect(page.locator("[data-hud-coins]")).to_have_text(
str(100 + economy.GRANT_CAP)
hud_number(100 + economy.GRANT_CAP)
)
assert page.locator("form[data-game-action='grant']").count() == 0
@@ -324,7 +334,9 @@ def test_infrastructure_buy_succeeds_when_eligible(alice):
confirm_btn.wait_for(state="visible")
confirm_btn.click()
expect(page.locator("form[data-game-action='infrastructure']")).to_have_count(0)
assert coins(page) == 100_000_000 - 25_000_000
from devplacepy.services.game import economy
assert coins(page) == 100_000_000 - economy.infra_for("registry").cost
def test_defense_upgrade_spends_coins(alice):
@@ -334,7 +346,7 @@ def test_defense_upgrade_spends_coins(alice):
upgrade = page.locator("form[data-game-action='defense'] button").first
upgrade.wait_for(state="visible")
upgrade.click()
expect(page.locator("[data-hud-coins]")).to_have_text("95000")
expect(page.locator("[data-hud-coins]")).to_have_text(hud_number(95000))
def test_cosmetics_buy_and_equip_flow(alice):
+26
View File
@@ -123,3 +123,29 @@ def test_create_notification_honors_in_app_pref(bob):
assert notifications.find_one(user_uid=uid, message=delivered) is not None
finally:
reset_notification_prefs(uid)
def test_quiz_attempt_type_listed_in_prefs(bob):
_, user = bob
uid = _user_notification_prefs(user["username"])["uid"]
reset_notification_prefs(uid)
prefs = get_notification_prefs(uid)
assert any(pref["key"] == "quiz_attempt" for pref in prefs)
def test_create_notification_honors_quiz_attempt_pref(bob):
_, user = bob
uid = _user_notification_prefs(user["username"])["uid"]
notifications = get_table("notifications")
reset_notification_prefs(uid)
try:
set_notification_pref(uid, "quiz_attempt", "in_app", False)
suppressed = f"quiz suppressed {generate_uid()}"
create_notification(uid, "quiz_attempt", suppressed, uid, "/quizzes")
assert notifications.find_one(user_uid=uid, message=suppressed) is None
set_notification_pref(uid, "quiz_attempt", "in_app", True)
delivered = f"quiz delivered {generate_uid()}"
create_notification(uid, "quiz_attempt", delivered, uid, "/quizzes")
assert notifications.find_one(user_uid=uid, message=delivered) is not None
finally:
reset_notification_prefs(uid)
+1
View File
@@ -0,0 +1 @@
# retoor <retoor@molodetz.nl>
+174
View File
@@ -0,0 +1,174 @@
# retoor <retoor@molodetz.nl>
from tests.conftest import BASE_URL
KIND_PICKER = "select[data-quiz-kind-picker]"
DIALOG_MESSAGE = ".dialog-message"
DIALOG_CONFIRM = ".dialog-confirm"
DIALOG_CANCEL = ".dialog-cancel"
OPTION_FIELDS = "[data-quiz-kind-fields='options']"
FREE_TEXT_FIELDS = "[data-quiz-kind-fields='free_text']"
NUMERIC_FIELDS = "[data-quiz-kind-fields='numeric']"
BOOLEAN_FIELDS = "[data-quiz-kind-fields='true_false']"
PUBLISH_BUTTON = ".quiz-publish button:has-text('Publish quiz')"
def _draft(page, title):
page.goto(f"{BASE_URL}/quizzes/new", wait_until="domcontentloaded")
page.locator("form.quiz-form").wait_for(state="visible")
page.fill("input[name='title']", title)
page.fill("textarea[name='description']", "Built in the browser.")
page.click("form.quiz-form button[type='submit']")
page.wait_for_url("**/edit", wait_until="domcontentloaded")
return page.url.rsplit("/", 2)[-2]
def _add_choice(page, prompt="Which journal mode allows concurrent readers?"):
form = page.locator("form[data-quiz-question-form]")
form.wait_for(state="visible")
page.select_option(KIND_PICKER, "single_choice")
page.fill("form[data-quiz-question-form] textarea[name='prompt']", prompt)
page.fill("form[data-quiz-question-form] textarea[name='options']", "DELETE\nWAL")
page.fill("form[data-quiz-question-form] input[name='correct_indexes']", "1")
page.click("form[data-quiz-question-form] button[type='submit']")
page.locator(".quiz-question").first.wait_for(state="visible")
def test_the_builder_opens_after_creating_a_draft(alice):
page, _ = alice
_draft(page, "Builder opens quiz")
page.locator(".quiz-builder-page").wait_for(state="visible")
page.locator("form[data-quiz-question-form]").wait_for(state="visible")
def test_the_kind_picker_lists_all_eight_kinds(alice):
page, _ = alice
_draft(page, "Builder kinds quiz")
picker = page.locator(KIND_PICKER)
picker.wait_for(state="visible")
assert picker.locator("option").count() == 8
def test_the_kind_picker_swaps_the_visible_fields(alice):
page, _ = alice
_draft(page, "Builder field swap quiz")
page.locator(KIND_PICKER).wait_for(state="visible")
page.select_option(KIND_PICKER, "single_choice")
assert page.locator(OPTION_FIELDS).is_visible()
assert not page.locator(FREE_TEXT_FIELDS).is_visible()
page.select_option(KIND_PICKER, "free_text")
assert page.locator(FREE_TEXT_FIELDS).is_visible()
assert not page.locator(OPTION_FIELDS).is_visible()
page.select_option(KIND_PICKER, "numeric")
assert page.locator(NUMERIC_FIELDS).is_visible()
page.select_option(KIND_PICKER, "true_false")
assert page.locator(BOOLEAN_FIELDS).is_visible()
def test_adding_a_question_renders_it_in_the_list(alice):
page, _ = alice
_draft(page, "Builder add quiz")
_add_choice(page)
question = page.locator(".quiz-question").first
assert "Which journal mode" in question.inner_text()
assert "Single choice" in question.inner_text()
def test_the_preview_marks_the_correct_option(alice):
page, _ = alice
_draft(page, "Builder preview quiz")
_add_choice(page)
page.locator(".quiz-preview-correct").first.wait_for(state="visible")
assert page.locator(".quiz-preview-correct").first.inner_text().strip() == "WAL"
def test_the_checklist_blocks_publishing_an_empty_quiz(alice):
page, _ = alice
_draft(page, "Builder checklist quiz")
page.locator(".quiz-checklist").wait_for(state="visible")
assert page.locator(PUBLISH_BUTTON).is_disabled()
def test_the_checklist_clears_once_the_quiz_is_complete(alice):
page, _ = alice
_draft(page, "Builder ready quiz")
_add_choice(page)
page.locator(".quiz-publish-ready").wait_for(state="visible")
assert page.locator(PUBLISH_BUTTON).is_enabled()
def test_the_move_controls_reorder_the_questions(alice):
page, _ = alice
_draft(page, "Builder reorder quiz")
_add_choice(page, "First question")
_add_choice(page, "Second question")
before = page.locator(".quiz-question-prompt").first.inner_text()
page.locator("button[aria-label='Move question down']").first.click()
page.wait_for_url("**/edit", wait_until="domcontentloaded")
page.locator(".quiz-question").first.wait_for(state="visible")
after = page.locator(".quiz-question-prompt").first.inner_text()
assert after != before
def test_deleting_a_question_removes_it(alice):
page, _ = alice
_draft(page, "Builder delete quiz")
_add_choice(page, "Doomed question")
assert page.locator(".quiz-question").count() == 1
page.locator(".quiz-question-actions button:has-text('Delete')").first.click()
page.locator(DIALOG_CONFIRM).wait_for(state="visible")
page.locator(DIALOG_CONFIRM).click()
page.wait_for_url("**/edit", wait_until="domcontentloaded")
page.locator(".empty-state").wait_for(state="visible")
assert page.locator(".quiz-question").count() == 0
def test_publishing_asks_for_confirmation(alice):
page, _ = alice
slug = _draft(page, "Builder confirm quiz")
_add_choice(page)
page.locator(PUBLISH_BUTTON).click()
page.locator(DIALOG_MESSAGE).wait_for(state="visible")
assert "permanent" in page.locator(DIALOG_MESSAGE).inner_text().lower()
page.locator(DIALOG_CANCEL).click()
page.goto(f"{BASE_URL}/quizzes/{slug}/edit", wait_until="domcontentloaded")
page.locator(".quiz-publish").wait_for(state="visible")
assert page.locator(".quiz-banner-locked").count() == 0
def test_accepting_the_confirmation_publishes(alice):
page, _ = alice
slug = _draft(page, "Builder publish quiz")
_add_choice(page)
page.locator(PUBLISH_BUTTON).click()
page.locator(DIALOG_CONFIRM).wait_for(state="visible")
page.locator(DIALOG_CONFIRM).click()
page.wait_for_url(f"**/quizzes/{slug}", wait_until="domcontentloaded")
page.locator("h1.quiz-detail-title").wait_for(state="visible")
def test_the_builder_is_read_only_after_publishing(alice):
page, _ = alice
slug = _draft(page, "Builder frozen quiz")
_add_choice(page)
page.locator(PUBLISH_BUTTON).click()
page.locator(DIALOG_CONFIRM).wait_for(state="visible")
page.locator(DIALOG_CONFIRM).click()
page.wait_for_url(f"**/quizzes/{slug}", wait_until="domcontentloaded")
page.goto(f"{BASE_URL}/quizzes/{slug}/edit", wait_until="domcontentloaded")
page.locator(".quiz-banner-locked").wait_for(state="visible")
assert page.locator("form[data-quiz-question-form]").count() == 0
assert page.locator(PUBLISH_BUTTON).count() == 0
assert page.locator(".quiz-question-actions").count() == 0
def test_the_detail_page_hides_edit_after_publishing(alice):
page, _ = alice
slug = _draft(page, "Builder detail frozen quiz")
_add_choice(page)
page.locator(PUBLISH_BUTTON).click()
page.locator(DIALOG_CONFIRM).wait_for(state="visible")
page.locator(DIALOG_CONFIRM).click()
page.wait_for_url(f"**/quizzes/{slug}", wait_until="domcontentloaded")
page.locator(".quiz-detail-actions").wait_for(state="visible")
assert page.locator(".quiz-detail-actions a:has-text('Edit')").count() == 0
+194
View File
@@ -0,0 +1,194 @@
# retoor <retoor@molodetz.nl>
import requests
from tests.conftest import BASE_URL
JSON = {"Accept": "application/json"}
LAYOUT = ".feed-layout"
LEFT = ".feed-layout > .sidebar-card"
CENTRE = ".feed-layout > .feed-main"
RIGHT = ".feed-layout > .feed-right"
NAV_ENTRY = ".topnav-link[href='/quizzes']"
NEW_QUIZ_BUTTON = ".quiz-actions a[href='/quizzes/new']"
DEVII_BUTTON = ".quiz-actions button[data-devii-open]"
SCOREBOARD = ".quiz-scoreboard-card"
def _seed_quiz(title="Hub e2e quiz"):
session = requests.Session()
name = f"hub{abs(hash(title)) % 10**9}"
session.post(
f"{BASE_URL}/auth/signup",
data={
"username": name,
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
},
allow_redirects=True,
)
slug = session.post(
f"{BASE_URL}/quizzes/create", data={"title": title, "description": "d"}, headers=JSON
).json()["data"]["slug"]
session.post(
f"{BASE_URL}/quizzes/{slug}/questions",
data={
"kind": "single_choice",
"prompt": "Which journal mode allows concurrent readers?",
"points": 2,
"options": "DELETE\nWAL",
"correct_indexes": "1",
},
headers=JSON,
)
session.post(f"{BASE_URL}/quizzes/{slug}/publish", headers=JSON)
return slug
def test_the_hub_renders_the_three_column_layout(page, app_server):
_seed_quiz("Layout e2e quiz")
page.goto(f"{BASE_URL}/quizzes", wait_until="domcontentloaded")
page.locator(LAYOUT).wait_for(state="visible")
assert page.locator(LEFT).count() == 1
assert page.locator(CENTRE).count() == 1
assert page.locator(RIGHT).count() == 1
def test_the_hub_has_a_visible_heading(page, app_server):
page.goto(f"{BASE_URL}/quizzes", wait_until="domcontentloaded")
heading = page.locator("h1.quiz-page-title")
heading.wait_for(state="visible")
assert heading.inner_text().strip() == "Quizzes"
def test_the_nav_entry_is_visible_to_a_guest_and_active(page, app_server):
page.goto(f"{BASE_URL}/quizzes", wait_until="domcontentloaded")
entry = page.locator(NAV_ENTRY)
entry.wait_for(state="visible")
assert "active" in (entry.get_attribute("class") or "")
def test_a_guest_sees_disabled_create_controls(page, app_server):
page.goto(f"{BASE_URL}/quizzes", wait_until="domcontentloaded")
page.locator(".quiz-actions").wait_for(state="visible")
assert page.locator(NEW_QUIZ_BUTTON).get_attribute("aria-disabled") == "true"
assert page.locator(DEVII_BUTTON).is_disabled()
def test_a_guest_still_sees_the_list_and_the_scoreboard(page, app_server):
_seed_quiz("Guest visible quiz")
page.goto(f"{BASE_URL}/quizzes", wait_until="domcontentloaded")
page.locator(SCOREBOARD).wait_for(state="visible")
assert page.locator(".quiz-card").count() >= 1
def test_a_member_sees_enabled_create_controls(alice):
page, _ = alice
page.goto(f"{BASE_URL}/quizzes", wait_until="domcontentloaded")
page.locator(NEW_QUIZ_BUTTON).wait_for(state="visible")
assert page.locator(NEW_QUIZ_BUTTON).get_attribute("aria-disabled") is None
assert page.locator(DEVII_BUTTON).is_enabled()
def test_a_member_sees_the_personal_filters(alice):
page, _ = alice
page.goto(f"{BASE_URL}/quizzes", wait_until="domcontentloaded")
page.locator(".sidebar-nav").wait_for(state="visible")
for target in ("todo", "done", "mine", "drafts"):
assert page.locator(f".sidebar-link[href='/quizzes?filter={target}']").count() == 1
def test_a_guest_sees_only_the_all_filter(page, app_server):
page.goto(f"{BASE_URL}/quizzes", wait_until="domcontentloaded")
page.locator(".sidebar-nav").wait_for(state="visible")
assert page.locator(".sidebar-link[href='/quizzes?filter=todo']").count() == 0
assert page.locator(".sidebar-link[href='/quizzes?filter=all']").count() == 1
def test_a_quiz_card_shows_its_state_badge(alice):
page, _ = alice
slug = _seed_quiz("Badge e2e quiz")
page.goto(f"{BASE_URL}/quizzes", wait_until="domcontentloaded")
card = page.locator(f".quiz-card:has(a[href='/quizzes/{slug}'])").first
card.wait_for(state="visible")
assert card.locator(".quiz-state-todo").count() == 1
def test_a_quiz_card_shows_its_meta_chips(page, app_server):
slug = _seed_quiz("Meta chips e2e quiz")
page.goto(f"{BASE_URL}/quizzes", wait_until="domcontentloaded")
card = page.locator(f".quiz-card:has(a[href='/quizzes/{slug}'])").first
card.wait_for(state="visible")
assert "1 questions" in card.inner_text()
assert "2 points" in card.inner_text()
def test_the_search_box_filters_the_list(page, app_server):
slug = _seed_quiz("Unmistakable pelican quiz")
page.goto(f"{BASE_URL}/quizzes?search=pelican", wait_until="domcontentloaded")
page.locator(".feed-posts").wait_for(state="visible")
assert page.locator(f".quiz-card:has(a[href='/quizzes/{slug}'])").count() == 1
def test_the_new_quiz_button_opens_the_create_form(alice):
page, _ = alice
page.goto(f"{BASE_URL}/quizzes", wait_until="domcontentloaded")
page.locator(NEW_QUIZ_BUTTON).click()
page.wait_for_url("**/quizzes/new", wait_until="domcontentloaded")
page.locator("form.quiz-form").wait_for(state="visible")
def test_the_devii_button_seeds_the_terminal_without_sending(alice):
page, _ = alice
page.goto(f"{BASE_URL}/quizzes", wait_until="domcontentloaded")
trigger = page.locator(DEVII_BUTTON)
trigger.wait_for(state="visible")
prompt = trigger.get_attribute("data-devii-prompt")
assert prompt and "import_quiz" in prompt
trigger.click()
terminal_input = page.locator("devii-terminal .devii-input")
terminal_input.wait_for(state="visible", timeout=10000)
assert terminal_input.input_value() == prompt
assert page.locator("devii-terminal .devii-line-user").count() == 0
page.evaluate("window.app && window.app.devii && window.app.devii.close()")
page.evaluate("window.localStorage.clear()")
def test_opening_a_card_reaches_the_detail_page(page, app_server):
slug = _seed_quiz("Card link e2e quiz")
page.goto(f"{BASE_URL}/quizzes", wait_until="domcontentloaded")
page.locator(f".quiz-card a.card-link[href='/quizzes/{slug}']").first.click()
page.wait_for_url(f"**/quizzes/{slug}", wait_until="domcontentloaded")
page.locator("h1.quiz-detail-title").wait_for(state="visible")
def test_the_detail_page_shows_the_stats_strip(page, app_server):
slug = _seed_quiz("Stats strip e2e quiz")
page.goto(f"{BASE_URL}/quizzes/{slug}", wait_until="domcontentloaded")
page.locator(".quiz-stats-strip").wait_for(state="visible")
assert page.locator(".quiz-stat").count() == 5
def test_the_detail_page_offers_a_start_button_to_a_member(alice):
page, _ = alice
slug = _seed_quiz("Start button e2e quiz")
page.goto(f"{BASE_URL}/quizzes/{slug}", wait_until="domcontentloaded")
button = page.locator(".quiz-detail-actions button:has-text('Start quiz')")
button.wait_for(state="visible")
assert button.is_enabled()
def test_the_detail_page_disables_start_for_a_guest(page, app_server):
slug = _seed_quiz("Guest start e2e quiz")
page.goto(f"{BASE_URL}/quizzes/{slug}", wait_until="domcontentloaded")
button = page.locator(".quiz-detail-actions button:has-text('Start quiz')")
button.wait_for(state="visible")
assert button.is_disabled()
def test_the_detail_page_carries_a_comment_section(page, app_server):
slug = _seed_quiz("Comment section e2e quiz")
page.goto(f"{BASE_URL}/quizzes/{slug}", wait_until="domcontentloaded")
page.locator(".comments-section").wait_for(state="visible")
+276
View File
@@ -0,0 +1,276 @@
# retoor <retoor@molodetz.nl>
import requests
from tests.conftest import BASE_URL
JSON = {"Accept": "application/json"}
CHOICE = {
"kind": "single_choice",
"prompt": "Which journal mode allows concurrent readers?",
"points": 2,
"options": "DELETE\nWAL",
"correct_indexes": "1",
}
NUMERIC = {
"kind": "numeric",
"prompt": "How many bytes are in a kibibyte?",
"points": 1,
"numeric_value": "1024",
"numeric_tolerance": "0",
}
DIALOG_CONFIRM = ".dialog-confirm"
_counter = [0]
def _author_quiz(title, questions=(CHOICE, NUMERIC), **settings):
_counter[0] += 1
name = f"play{abs(hash(title)) % 10**8}{_counter[0]}"
session = requests.Session()
session.post(
f"{BASE_URL}/auth/signup",
data={
"username": name,
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
},
allow_redirects=True,
)
payload = {"title": title, "description": "d", **settings}
slug = session.post(f"{BASE_URL}/quizzes/create", data=payload, headers=JSON).json()["data"][
"slug"
]
for question in questions:
session.post(f"{BASE_URL}/quizzes/{slug}/questions", data=question, headers=JSON)
session.post(f"{BASE_URL}/quizzes/{slug}/publish", headers=JSON)
return slug
def _start(page, slug):
page.goto(f"{BASE_URL}/quizzes/{slug}", wait_until="domcontentloaded")
page.locator(".quiz-detail-actions button:has-text('Start quiz')").click()
page.wait_for_url("**/attempts/**", wait_until="domcontentloaded")
page.locator("dp-quiz-player").wait_for(state="visible")
def test_starting_opens_the_player(alice):
page, _ = alice
slug = _author_quiz("Player opens quiz")
_start(page, slug)
assert page.locator(".quiz-question").count() == 2
page.locator(".quiz-hud").wait_for(state="visible")
def test_the_player_renders_a_real_form_per_question(alice):
page, _ = alice
slug = _author_quiz("Player forms quiz")
_start(page, slug)
forms = page.locator("form[data-quiz-answer-form]")
assert forms.count() == 2
assert forms.first.get_attribute("method").lower() == "post"
def test_answering_a_choice_shows_the_grade_in_place(alice):
page, _ = alice
slug = _author_quiz("Player grade quiz")
_start(page, slug)
question = page.locator(".quiz-question-single_choice").first
question.locator("input[type='radio']").last.check()
question.locator("button:has-text('Submit answer')").click()
question.locator(".quiz-grade-correct").wait_for(state="visible")
assert "Correct" in question.locator(".quiz-grade-verdict").inner_text()
def test_a_wrong_answer_is_reported(alice):
page, _ = alice
slug = _author_quiz("Player wrong quiz")
_start(page, slug)
question = page.locator(".quiz-question-single_choice").first
question.locator("input[type='radio']").first.check()
question.locator("button:has-text('Submit answer')").click()
question.locator(".quiz-grade-wrong").wait_for(state="visible")
def test_the_hud_updates_after_an_answer(alice):
page, _ = alice
slug = _author_quiz("Player hud quiz")
_start(page, slug)
before = page.locator("[data-quiz-progress]").inner_text()
question = page.locator(".quiz-question-single_choice").first
question.locator("input[type='radio']").last.check()
question.locator("button:has-text('Submit answer')").click()
question.locator(".quiz-grade").wait_for(state="visible")
page.wait_for_function(
"before => document.querySelector('[data-quiz-progress]').textContent !== before",
arg=before,
)
assert "1 / 2 answered" in page.locator("[data-quiz-progress]").inner_text()
assert "2 / 3 points" in page.locator("[data-quiz-score]").inner_text()
def test_an_answered_question_cannot_be_resubmitted(alice):
page, _ = alice
slug = _author_quiz("Player once quiz")
_start(page, slug)
question = page.locator(".quiz-question-single_choice").first
question.locator("input[type='radio']").last.check()
question.locator("button:has-text('Submit answer')").click()
question.locator(".quiz-grade").wait_for(state="visible")
assert question.locator("button:has-text('Submit answer')").count() == 0
assert question.locator("input[type='radio']").first.is_disabled()
def test_a_numeric_question_takes_a_typed_answer(alice):
page, _ = alice
slug = _author_quiz("Player numeric quiz")
_start(page, slug)
question = page.locator(".quiz-question-numeric").first
question.locator("input[name='answer_text']").fill("1024")
question.locator("button:has-text('Submit answer')").click()
question.locator(".quiz-grade-correct").wait_for(state="visible")
def test_the_timer_is_rendered_for_a_limited_quiz(alice):
page, _ = alice
slug = _author_quiz("Player timer quiz", time_limit_seconds="600")
_start(page, slug)
timer = page.locator("[data-quiz-timer]")
timer.wait_for(state="visible")
assert timer.inner_text().strip()
def test_no_timer_is_rendered_without_a_limit(alice):
page, _ = alice
slug = _author_quiz("Player untimed quiz")
_start(page, slug)
page.locator(".quiz-hud").wait_for(state="visible")
assert page.locator("[data-quiz-timer]").count() == 0
def test_the_answer_key_is_not_in_the_page_source(alice):
page, _ = alice
slug = _author_quiz("Player hidden key quiz", questions=(NUMERIC,), reveal_answers="0")
_start(page, slug)
assert "1024" not in page.content()
def test_reloading_resumes_the_same_attempt(alice):
page, _ = alice
slug = _author_quiz("Player resume quiz")
_start(page, slug)
attempt_url = page.url
question = page.locator(".quiz-question-single_choice").first
question.locator("input[type='radio']").last.check()
question.locator("button:has-text('Submit answer')").click()
question.locator(".quiz-grade").wait_for(state="visible")
page.goto(f"{BASE_URL}/quizzes/{slug}", wait_until="domcontentloaded")
page.locator(".quiz-detail-actions a:has-text('Resume attempt')").click()
page.wait_for_url("**/attempts/**", wait_until="domcontentloaded")
assert page.url == attempt_url
assert page.locator(".quiz-question-answered").count() == 1
def test_finishing_reaches_the_results_page(alice):
page, _ = alice
slug = _author_quiz("Player finish quiz", pass_percent="50")
_start(page, slug)
for question in (".quiz-question-single_choice", ".quiz-question-numeric"):
node = page.locator(question).first
if "numeric" in question:
node.locator("input[name='answer_text']").fill("1024")
else:
node.locator("input[type='radio']").last.check()
node.locator("button:has-text('Submit answer')").click()
node.locator(".quiz-grade").wait_for(state="visible")
page.locator("form[data-quiz-finish-form] button").click()
page.locator(DIALOG_CONFIRM).wait_for(state="visible")
page.locator(DIALOG_CONFIRM).click()
page.wait_for_url("**/results", wait_until="domcontentloaded")
page.locator(".quiz-score-card").wait_for(state="visible")
assert "100" in page.locator(".quiz-score-percent").inner_text()
def test_the_results_page_shows_the_pass_verdict(alice):
page, _ = alice
slug = _author_quiz("Player verdict quiz", questions=(NUMERIC,), pass_percent="50")
_start(page, slug)
node = page.locator(".quiz-question-numeric").first
node.locator("input[name='answer_text']").fill("1024")
node.locator("button:has-text('Submit answer')").click()
node.locator(".quiz-grade").wait_for(state="visible")
page.locator("form[data-quiz-finish-form] button").click()
page.locator(DIALOG_CONFIRM).wait_for(state="visible")
page.locator(DIALOG_CONFIRM).click()
page.wait_for_url("**/results", wait_until="domcontentloaded")
page.locator(".quiz-verdict-pass").wait_for(state="visible")
def test_the_results_page_reviews_every_answer(alice):
page, _ = alice
slug = _author_quiz("Player review quiz", allow_review="1")
_start(page, slug)
node = page.locator(".quiz-question-single_choice").first
node.locator("input[type='radio']").last.check()
node.locator("button:has-text('Submit answer')").click()
node.locator(".quiz-grade").wait_for(state="visible")
page.locator("form[data-quiz-finish-form] button").click()
page.locator(DIALOG_CONFIRM).wait_for(state="visible")
page.locator(DIALOG_CONFIRM).click()
page.wait_for_url("**/results", wait_until="domcontentloaded")
page.locator(".quiz-review").wait_for(state="visible")
assert page.locator(".quiz-review-item").count() == 2
def test_the_review_is_hidden_when_the_author_disabled_it(alice):
page, _ = alice
slug = _author_quiz("Player no review quiz", questions=(NUMERIC,), allow_review="0")
_start(page, slug)
node = page.locator(".quiz-question-numeric").first
node.locator("input[name='answer_text']").fill("1024")
node.locator("button:has-text('Submit answer')").click()
node.locator(".quiz-grade").wait_for(state="visible")
page.locator("form[data-quiz-finish-form] button").click()
page.locator(DIALOG_CONFIRM).wait_for(state="visible")
page.locator(DIALOG_CONFIRM).click()
page.wait_for_url("**/results", wait_until="domcontentloaded")
page.locator(".quiz-review-disabled").wait_for(state="visible")
def test_the_results_page_offers_a_replay(alice):
page, _ = alice
slug = _author_quiz("Player replay quiz", questions=(NUMERIC,))
_start(page, slug)
node = page.locator(".quiz-question-numeric").first
node.locator("input[name='answer_text']").fill("1024")
node.locator("button:has-text('Submit answer')").click()
node.locator(".quiz-grade").wait_for(state="visible")
page.locator("form[data-quiz-finish-form] button").click()
page.locator(DIALOG_CONFIRM).wait_for(state="visible")
page.locator(DIALOG_CONFIRM).click()
page.wait_for_url("**/results", wait_until="domcontentloaded")
page.locator(".quiz-results-actions button:has-text('Play again')").click()
page.wait_for_url("**/attempts/**", wait_until="domcontentloaded")
page.locator("dp-quiz-player").wait_for(state="visible")
def test_the_hub_shows_the_completed_badge_afterwards(alice):
page, _ = alice
slug = _author_quiz("Player badge quiz", questions=(NUMERIC,))
_start(page, slug)
node = page.locator(".quiz-question-numeric").first
node.locator("input[name='answer_text']").fill("1024")
node.locator("button:has-text('Submit answer')").click()
node.locator(".quiz-grade").wait_for(state="visible")
page.locator("form[data-quiz-finish-form] button").click()
page.locator(DIALOG_CONFIRM).wait_for(state="visible")
page.locator(DIALOG_CONFIRM).click()
page.wait_for_url("**/results", wait_until="domcontentloaded")
page.goto(f"{BASE_URL}/quizzes?filter=done", wait_until="domcontentloaded")
card = page.locator(f".quiz-card:has(a[href='/quizzes/{slug}'])").first
card.wait_for(state="visible")
assert card.locator(".quiz-state-done").count() == 1