forked from retoor/devplacepy
Quiiz system
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
@@ -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
|
||||
@@ -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")
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user