deck contribution + game invite: write all FTs red — TDD
5-sprint outside-in FT suite. Each FT class drives one sprint: Sprint 1 (DeckContributionTest): role confirmation sets TableSeat.deck_variant to the gamer's equipped deck; Game Kit tooltip shows game name + In-Use status. Sprint 2 (DeckInUseGameKitTest): DON btn-disabled w.o DOFF toggle for in-use deck; tooltip names the game; non-contributing deck retains normal DON/DOFF. Sprint 3 (GameInviteNotificationTest, @two-browser): invite_gamer() creates BillPost(kind=INVITE) for invitee; INVITE: <room> link appears in My Posts. Sprint 4 (GameInviteBillPostTest): /billboard/post/<pk>/ renders _billpost_invite partial; BYE dismisses; OK shows join-guard when valid deck is equipped. Sprint 5 (GameInviteDeckValidationTest): OK btn-disabled + tooltip when no valid deck; confirming join assigns deck to seat and locks Game Kit DON. New model surface: billboard.BillPost (kind, recipient, room, invite, dismissed) New field: epic.TableSeat.deck_variant FK → DeckVariant Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
216
src/functional_tests/test_deck_contribution.py
Normal file
216
src/functional_tests/test_deck_contribution.py
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
"""
|
||||||
|
Functional tests: deck-to-seat contribution mechanics.
|
||||||
|
|
||||||
|
Sprint 1 (DeckContributionTest):
|
||||||
|
Confirming a role in ROLE SELECT assigns the gamer's equipped deck to the
|
||||||
|
TableSeat. The Game Kit immediately reflects the in-use state: the deck
|
||||||
|
card gains the game name in its tooltip and the micro-status reads "In-Use".
|
||||||
|
|
||||||
|
Sprint 2 (DeckInUseGameKitTest):
|
||||||
|
With a deck already assigned to an active seat the Game Kit DON button is
|
||||||
|
btn-disabled and no DOFF toggle is shown (unlike the normal equipped state
|
||||||
|
where DOFF is visible). Clicking the card shows a tooltip naming the game.
|
||||||
|
A second deck that is NOT assigned to any active seat has normal DON/DOFF
|
||||||
|
behaviour.
|
||||||
|
"""
|
||||||
|
import time
|
||||||
|
|
||||||
|
from django.test import tag
|
||||||
|
from selenium.webdriver.common.by import By
|
||||||
|
|
||||||
|
from apps.applets.models import Applet
|
||||||
|
from apps.epic.models import DeckVariant, GateSlot, Room, TableSeat
|
||||||
|
from apps.lyric.models import User
|
||||||
|
from functional_tests.base import FunctionalTest
|
||||||
|
from functional_tests.test_room_role_select import _fill_room_via_orm
|
||||||
|
from functional_tests.test_room_sig_select import _assign_all_roles
|
||||||
|
|
||||||
|
FOUNDER_EMAIL = "founder@test.io"
|
||||||
|
GAMER_EMAIL = "gamer@test.io"
|
||||||
|
|
||||||
|
|
||||||
|
def _equip_earthman(user):
|
||||||
|
earthman = DeckVariant.objects.get(slug="earthman")
|
||||||
|
user.equipped_deck = earthman
|
||||||
|
user.save(update_fields=["equipped_deck"])
|
||||||
|
return earthman
|
||||||
|
|
||||||
|
|
||||||
|
def _room_at_role_select(name="Wildfire"):
|
||||||
|
"""Create a room filled to 6 and at ROLE_SELECT table status."""
|
||||||
|
founder, _ = User.objects.get_or_create(email=FOUNDER_EMAIL)
|
||||||
|
room = Room.objects.create(name=name, owner=founder)
|
||||||
|
emails = [FOUNDER_EMAIL, GAMER_EMAIL,
|
||||||
|
"b@test.io", "c@test.io", "d@test.io", "e@test.io"]
|
||||||
|
_fill_room_via_orm(room, emails)
|
||||||
|
for slot in room.gate_slots.all():
|
||||||
|
if slot.gamer:
|
||||||
|
_equip_earthman(slot.gamer)
|
||||||
|
room.table_status = Room.ROLE_SELECT
|
||||||
|
room.save()
|
||||||
|
return room, founder
|
||||||
|
|
||||||
|
|
||||||
|
# ── Sprint 1 ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class DeckContributionTest(FunctionalTest):
|
||||||
|
"""Sprint 1: role confirmation → TableSeat.deck_variant set; Game Kit shows In-Use."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.browser.set_window_size(800, 1200)
|
||||||
|
for slug, name, ctx in [
|
||||||
|
("game-kit", "Game Kit", "gameboard"),
|
||||||
|
("gk-decks", "Card Decks","game-kit"),
|
||||||
|
]:
|
||||||
|
Applet.objects.get_or_create(slug=slug, defaults={"name": name, "context": ctx})
|
||||||
|
|
||||||
|
def test_confirming_role_assigns_equipped_deck_to_seat(self):
|
||||||
|
"""After the gamer confirms a role, Game Kit shows the Earthman deck as 'In-Use'
|
||||||
|
with the game name in the deck tooltip, and the micro-status changes from
|
||||||
|
'Equipped' to 'In-Use'.
|
||||||
|
"""
|
||||||
|
room, founder = _room_at_role_select()
|
||||||
|
gamer = User.objects.get(email=GAMER_EMAIL)
|
||||||
|
|
||||||
|
# Gamer logs in and navigates to the role-select room
|
||||||
|
session_key = self.create_pre_authenticated_session(GAMER_EMAIL)
|
||||||
|
self.browser.get(self.live_server_url)
|
||||||
|
self.browser.add_cookie({"name": "sessionid", "value": session_key})
|
||||||
|
self.browser.get(self.live_server_url + f"/gameboard/room/{room.pk}/")
|
||||||
|
|
||||||
|
# Gamer confirms a role (NC — slot 2 is theirs)
|
||||||
|
role_btn = self.wait_for(
|
||||||
|
lambda: self.browser.find_element(By.CSS_SELECTOR, ".role-card[data-role='NC'] .btn-confirm")
|
||||||
|
)
|
||||||
|
role_btn.click()
|
||||||
|
|
||||||
|
# Deck is now assigned to the seat in the DB
|
||||||
|
self.wait_for(lambda: self.assertTrue(
|
||||||
|
TableSeat.objects.filter(
|
||||||
|
gamer=gamer,
|
||||||
|
room=room,
|
||||||
|
deck_variant=gamer.equipped_deck,
|
||||||
|
).exists(),
|
||||||
|
"TableSeat.deck_variant was not set after role confirmation",
|
||||||
|
))
|
||||||
|
|
||||||
|
# Navigate to Game Kit → Card Decks to verify UI state
|
||||||
|
self.browser.get(self.live_server_url + "/gameboard/game-kit/")
|
||||||
|
decks_btn = self.wait_for(
|
||||||
|
lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_kit_card_deck")
|
||||||
|
)
|
||||||
|
decks_btn.click()
|
||||||
|
|
||||||
|
earthman_card = self.wait_for(
|
||||||
|
lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_kit_earthman_deck")
|
||||||
|
)
|
||||||
|
earthman_card.click() # open tooltip
|
||||||
|
|
||||||
|
# Tooltip shows the game name
|
||||||
|
tooltip = self.wait_for(
|
||||||
|
lambda: self.browser.find_element(By.CSS_SELECTOR, ".tt-deck-game-name")
|
||||||
|
)
|
||||||
|
self.assertIn(room.name.upper(), tooltip.text.upper())
|
||||||
|
|
||||||
|
# Micro-status reads "In-Use", not "Equipped"
|
||||||
|
micro_status = self.wait_for(
|
||||||
|
lambda: self.browser.find_element(
|
||||||
|
By.CSS_SELECTOR, "#id_kit_earthman_deck .deck-micro-status"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertIn("IN-USE", micro_status.text.upper())
|
||||||
|
self.assertNotIn("EQUIPPED", micro_status.text.upper())
|
||||||
|
|
||||||
|
|
||||||
|
# ── Sprint 2 ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class DeckInUseGameKitTest(FunctionalTest):
|
||||||
|
"""Sprint 2: DON disabled + no DOFF toggle for in-use deck; second deck unaffected."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.browser.set_window_size(800, 1200)
|
||||||
|
for slug, name, ctx in [
|
||||||
|
("game-kit", "Game Kit", "gameboard"),
|
||||||
|
("gk-decks", "Card Decks","game-kit"),
|
||||||
|
]:
|
||||||
|
Applet.objects.get_or_create(slug=slug, defaults={"name": name, "context": ctx})
|
||||||
|
|
||||||
|
def _setup_in_use_deck(self):
|
||||||
|
"""Create a seated gamer whose Earthman deck is already assigned to a seat."""
|
||||||
|
room, founder = _room_at_role_select()
|
||||||
|
gamer = User.objects.get(email=GAMER_EMAIL)
|
||||||
|
earthman = _equip_earthman(gamer)
|
||||||
|
# Assign deck directly (Sprint 1 must be green first)
|
||||||
|
seat = TableSeat.objects.create(gamer=gamer, room=room, role="NC", deck_variant=earthman)
|
||||||
|
return gamer, earthman, room, seat
|
||||||
|
|
||||||
|
def test_don_is_disabled_and_doff_absent_for_in_use_deck(self):
|
||||||
|
"""DON button carries btn-disabled; DOFF is not rendered at all (not just disabled)."""
|
||||||
|
gamer, earthman, room, seat = self._setup_in_use_deck()
|
||||||
|
session_key = self.create_pre_authenticated_session(GAMER_EMAIL)
|
||||||
|
self.browser.get(self.live_server_url)
|
||||||
|
self.browser.add_cookie({"name": "sessionid", "value": session_key})
|
||||||
|
self.browser.get(self.live_server_url + "/gameboard/game-kit/")
|
||||||
|
self.wait_for(
|
||||||
|
lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_kit_card_deck")
|
||||||
|
).click()
|
||||||
|
self.wait_for(
|
||||||
|
lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_kit_earthman_deck")
|
||||||
|
).click()
|
||||||
|
|
||||||
|
# DON is disabled
|
||||||
|
don_btn = self.wait_for(
|
||||||
|
lambda: self.browser.find_element(
|
||||||
|
By.CSS_SELECTOR, f"#id_kit_earthman_deck .btn-equip"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertIn("btn-disabled", don_btn.get_attribute("class"))
|
||||||
|
|
||||||
|
# DOFF button is not present (not just disabled — entirely absent)
|
||||||
|
doff_btns = self.browser.find_elements(
|
||||||
|
By.CSS_SELECTOR, f"#id_kit_earthman_deck .btn-unequip:not(.btn-disabled)"
|
||||||
|
)
|
||||||
|
self.assertEqual(len(doff_btns), 0, "DOFF button should not be shown for an in-use deck")
|
||||||
|
|
||||||
|
def test_tooltip_names_the_game_for_in_use_deck(self):
|
||||||
|
"""Opening an in-use deck's tooltip shows the room name it is contributing to."""
|
||||||
|
gamer, earthman, room, seat = self._setup_in_use_deck()
|
||||||
|
session_key = self.create_pre_authenticated_session(GAMER_EMAIL)
|
||||||
|
self.browser.get(self.live_server_url)
|
||||||
|
self.browser.add_cookie({"name": "sessionid", "value": session_key})
|
||||||
|
self.browser.get(self.live_server_url + "/gameboard/game-kit/")
|
||||||
|
self.wait_for(
|
||||||
|
lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_kit_card_deck")
|
||||||
|
).click()
|
||||||
|
self.wait_for(
|
||||||
|
lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_kit_earthman_deck")
|
||||||
|
).click()
|
||||||
|
game_label = self.wait_for(
|
||||||
|
lambda: self.browser.find_element(By.CSS_SELECTOR, ".tt-deck-game-name")
|
||||||
|
)
|
||||||
|
self.assertIn(room.name.upper(), game_label.text.upper())
|
||||||
|
|
||||||
|
def test_non_contributing_deck_has_normal_don_doff(self):
|
||||||
|
"""A deck not assigned to any active seat shows the normal DON/DOFF apparatus."""
|
||||||
|
gamer, earthman, room, seat = self._setup_in_use_deck()
|
||||||
|
# Unlock Fiorentine for the gamer so it appears in Game Kit
|
||||||
|
fiorentine = DeckVariant.objects.get(slug="fiorentine-minchiate")
|
||||||
|
gamer.unlocked_decks.add(fiorentine)
|
||||||
|
session_key = self.create_pre_authenticated_session(GAMER_EMAIL)
|
||||||
|
self.browser.get(self.live_server_url)
|
||||||
|
self.browser.add_cookie({"name": "sessionid", "value": session_key})
|
||||||
|
self.browser.get(self.live_server_url + "/gameboard/game-kit/")
|
||||||
|
self.wait_for(
|
||||||
|
lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_kit_card_deck")
|
||||||
|
).click()
|
||||||
|
self.wait_for(
|
||||||
|
lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_kit_fiorentine_deck")
|
||||||
|
).click()
|
||||||
|
don_btn = self.wait_for(
|
||||||
|
lambda: self.browser.find_element(
|
||||||
|
By.CSS_SELECTOR, "#id_kit_fiorentine_deck .btn-equip"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertNotIn("btn-disabled", don_btn.get_attribute("class"))
|
||||||
354
src/functional_tests/test_game_invite.py
Normal file
354
src/functional_tests/test_game_invite.py
Normal file
@@ -0,0 +1,354 @@
|
|||||||
|
"""
|
||||||
|
Functional tests: game-room invite flow via BillPost.
|
||||||
|
|
||||||
|
BillPost is a new billboard.BillPost model (kind=INVITE for this sprint;
|
||||||
|
more kinds to follow in later feature sprints). It is the first of a series
|
||||||
|
of Post-types that will grow through coming sprints. The existing
|
||||||
|
dashboard.Post model will be renamed (to Message or similar) when the WS
|
||||||
|
messaging sprint lands; this new system's naming takes precedence.
|
||||||
|
|
||||||
|
Sprint 3 (GameInviteNotificationTest) — @two-browser:
|
||||||
|
Founder sends an email invite from the room page. invite_gamer() creates a
|
||||||
|
BillPost(kind=INVITE) for the invitee. Invitee sees "INVITE: <room>" as a
|
||||||
|
link in the My Posts applet on the billboard page.
|
||||||
|
|
||||||
|
Sprint 4 (GameInviteBillPostTest):
|
||||||
|
Clicking the invite post navigates to /billboard/post/<pk>/. The page
|
||||||
|
renders a _billpost_invite.html partial with room info. OK and BYE
|
||||||
|
(.btn-confirm / .btn-abandon) appear next to the title.
|
||||||
|
BYE marks the BillPost dismissed; the post is no longer listed in My Posts.
|
||||||
|
OK shows a Log-Out-style confirm guard ("Join game? OK / NVM") if the
|
||||||
|
invitee has a valid (non-contributing) deck equipped.
|
||||||
|
|
||||||
|
Sprint 5 (GameInviteDeckValidationTest):
|
||||||
|
If the invitee has no deck equipped, or their equipped deck is already
|
||||||
|
assigned to an active seat, the OK button is immediately btn-disabled
|
||||||
|
(no guard shown) and a tooltip explains the situation.
|
||||||
|
When a valid deck is equipped and the invitee confirms, they are redirected
|
||||||
|
to the room and their deck is assigned to a seat.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
|
from django.test import tag
|
||||||
|
from selenium import webdriver
|
||||||
|
from selenium.webdriver.common.by import By
|
||||||
|
|
||||||
|
from apps.applets.models import Applet
|
||||||
|
from apps.billboard.models import BillPost
|
||||||
|
from apps.epic.models import DeckVariant, GateSlot, Room, RoomInvite, TableSeat
|
||||||
|
from apps.lyric.models import User
|
||||||
|
from functional_tests.base import FunctionalTest
|
||||||
|
from functional_tests.test_room_role_select import _fill_room_via_orm
|
||||||
|
|
||||||
|
FOUNDER_EMAIL = "founder@test.io"
|
||||||
|
INVITEE_EMAIL = "invitee@test.io"
|
||||||
|
|
||||||
|
|
||||||
|
def _applets():
|
||||||
|
for slug, name, ctx in [
|
||||||
|
("my-posts", "My Posts", "billboard"),
|
||||||
|
("billboard-my-scrolls", "My Scrolls","billboard"),
|
||||||
|
]:
|
||||||
|
Applet.objects.get_or_create(slug=slug, defaults={"name": name, "context": ctx})
|
||||||
|
|
||||||
|
|
||||||
|
def _make_open_room(name="Wildfire"):
|
||||||
|
founder, _ = User.objects.get_or_create(email=FOUNDER_EMAIL)
|
||||||
|
room = Room.objects.create(name=name, owner=founder)
|
||||||
|
_fill_room_via_orm(room, [FOUNDER_EMAIL,
|
||||||
|
"b@test.io", "c@test.io", "d@test.io", "e@test.io", "f@test.io"])
|
||||||
|
return room, founder
|
||||||
|
|
||||||
|
|
||||||
|
def _make_second_browser():
|
||||||
|
opts = webdriver.FirefoxOptions()
|
||||||
|
if os.environ.get("HEADLESS"):
|
||||||
|
opts.add_argument("--headless")
|
||||||
|
b = webdriver.Firefox(options=opts)
|
||||||
|
b.set_window_size(800, 1200)
|
||||||
|
return b
|
||||||
|
|
||||||
|
|
||||||
|
# ── Sprint 3 ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@tag("two-browser")
|
||||||
|
class GameInviteNotificationTest(FunctionalTest):
|
||||||
|
"""Sprint 3: founder sends invite → INVITE: BillPost appears in invitee's My Posts."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.browser.set_window_size(800, 1200)
|
||||||
|
self.second_browser = _make_second_browser()
|
||||||
|
_applets()
|
||||||
|
User.objects.get_or_create(email=INVITEE_EMAIL)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.second_browser.quit()
|
||||||
|
super().tearDown()
|
||||||
|
|
||||||
|
def test_invite_creates_bill_post_in_my_posts(self):
|
||||||
|
"""Founder sends email invite; invitee sees 'INVITE: Wildfire' link in My Posts."""
|
||||||
|
room, founder = _make_open_room()
|
||||||
|
|
||||||
|
# Founder: log in, navigate to room, send invite
|
||||||
|
founder_session = self.create_pre_authenticated_session(FOUNDER_EMAIL)
|
||||||
|
self.browser.get(self.live_server_url)
|
||||||
|
self.browser.add_cookie({"name": "sessionid", "value": founder_session})
|
||||||
|
self.browser.get(self.live_server_url + f"/gameboard/room/{room.pk}/")
|
||||||
|
|
||||||
|
invite_input = self.wait_for(
|
||||||
|
lambda: self.browser.find_element(By.CSS_SELECTOR, "input[name='invitee_email']")
|
||||||
|
)
|
||||||
|
invite_input.send_keys(INVITEE_EMAIL)
|
||||||
|
self.browser.find_element(By.CSS_SELECTOR, "#id_invite_btn").click()
|
||||||
|
|
||||||
|
# A BillPost(kind=INVITE) is created for the invitee
|
||||||
|
self.wait_for(lambda: self.assertTrue(
|
||||||
|
BillPost.objects.filter(
|
||||||
|
kind=BillPost.INVITE,
|
||||||
|
recipient__email=INVITEE_EMAIL,
|
||||||
|
room=room,
|
||||||
|
dismissed=False,
|
||||||
|
).exists(),
|
||||||
|
"BillPost(kind=INVITE) was not created for the invitee",
|
||||||
|
))
|
||||||
|
|
||||||
|
# Invitee: log in to billboard, see "INVITE: Wildfire" in My Posts applet
|
||||||
|
invitee = User.objects.get(email=INVITEE_EMAIL)
|
||||||
|
invitee_session = self.create_pre_authenticated_session(INVITEE_EMAIL)
|
||||||
|
self.second_browser.get(self.live_server_url)
|
||||||
|
self.second_browser.add_cookie({"name": "sessionid", "value": invitee_session})
|
||||||
|
self.second_browser.get(self.live_server_url + "/billboard/")
|
||||||
|
|
||||||
|
my_posts = self.wait_for(
|
||||||
|
lambda: self.second_browser.find_element(By.CSS_SELECTOR, "#id_applet_my_posts")
|
||||||
|
)
|
||||||
|
invite_link = self.wait_for(
|
||||||
|
lambda: my_posts.find_element(By.PARTIAL_LINK_TEXT, "INVITE:")
|
||||||
|
)
|
||||||
|
self.assertIn("WILDFIRE", invite_link.text.upper())
|
||||||
|
|
||||||
|
# Link goes to /billboard/post/<pk>/
|
||||||
|
post = BillPost.objects.get(kind=BillPost.INVITE, recipient__email=INVITEE_EMAIL)
|
||||||
|
self.assertIn(f"/billboard/post/{post.pk}/", invite_link.get_attribute("href"))
|
||||||
|
|
||||||
|
|
||||||
|
# ── Sprint 4 ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class GameInviteBillPostTest(FunctionalTest):
|
||||||
|
"""Sprint 4: BillPost invite page — _billpost_invite.html partial, OK/BYE, BYE dismisses."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.browser.set_window_size(800, 1200)
|
||||||
|
_applets()
|
||||||
|
|
||||||
|
def _make_invite_post(self, equip_deck=False):
|
||||||
|
room, founder = _make_open_room()
|
||||||
|
invitee, _ = User.objects.get_or_create(email=INVITEE_EMAIL)
|
||||||
|
invite = RoomInvite.objects.create(
|
||||||
|
room=room, inviter=founder, invitee_email=INVITEE_EMAIL,
|
||||||
|
status=RoomInvite.PENDING,
|
||||||
|
)
|
||||||
|
post = BillPost.objects.create(
|
||||||
|
kind=BillPost.INVITE, recipient=invitee, room=room, invite=invite,
|
||||||
|
)
|
||||||
|
if equip_deck:
|
||||||
|
earthman = DeckVariant.objects.get(slug="earthman")
|
||||||
|
invitee.equipped_deck = earthman
|
||||||
|
invitee.unlocked_decks.add(earthman)
|
||||||
|
invitee.save(update_fields=["equipped_deck"])
|
||||||
|
return invitee, post, room
|
||||||
|
|
||||||
|
def test_billpost_page_shows_invite_partial(self):
|
||||||
|
"""The BillPost page at /billboard/post/<pk>/ renders the invite partial with room info."""
|
||||||
|
invitee, post, room = self._make_invite_post()
|
||||||
|
session_key = self.create_pre_authenticated_session(INVITEE_EMAIL)
|
||||||
|
self.browser.get(self.live_server_url)
|
||||||
|
self.browser.add_cookie({"name": "sessionid", "value": session_key})
|
||||||
|
self.browser.get(self.live_server_url + f"/billboard/post/{post.pk}/")
|
||||||
|
|
||||||
|
# Page title / heading shows "INVITE: Wildfire"
|
||||||
|
heading = self.wait_for(
|
||||||
|
lambda: self.browser.find_element(By.CSS_SELECTOR, ".billpost-title")
|
||||||
|
)
|
||||||
|
self.assertIn("INVITE", heading.text.upper())
|
||||||
|
self.assertIn(room.name.upper(), heading.text.upper())
|
||||||
|
|
||||||
|
# Invite partial is present
|
||||||
|
self.wait_for(
|
||||||
|
lambda: self.browser.find_element(By.CSS_SELECTOR, ".billpost-invite")
|
||||||
|
)
|
||||||
|
|
||||||
|
# OK and BYE buttons are present
|
||||||
|
self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".btn-confirm"))
|
||||||
|
self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".btn-abandon"))
|
||||||
|
|
||||||
|
def test_bye_dismisses_invite_and_removes_from_my_posts(self):
|
||||||
|
"""Clicking BYE marks the BillPost dismissed and it no longer appears in My Posts."""
|
||||||
|
invitee, post, room = self._make_invite_post()
|
||||||
|
session_key = self.create_pre_authenticated_session(INVITEE_EMAIL)
|
||||||
|
self.browser.get(self.live_server_url)
|
||||||
|
self.browser.add_cookie({"name": "sessionid", "value": session_key})
|
||||||
|
self.browser.get(self.live_server_url + f"/billboard/post/{post.pk}/")
|
||||||
|
|
||||||
|
bye_btn = self.wait_for(
|
||||||
|
lambda: self.browser.find_element(By.CSS_SELECTOR, ".btn-abandon")
|
||||||
|
)
|
||||||
|
bye_btn.click()
|
||||||
|
|
||||||
|
# BillPost is now dismissed in DB
|
||||||
|
post.refresh_from_db()
|
||||||
|
self.assertTrue(post.dismissed)
|
||||||
|
|
||||||
|
# Redirected to billboard; post no longer in My Posts
|
||||||
|
self.wait_for(lambda: self.assertIn("/billboard/", self.browser.current_url))
|
||||||
|
my_posts = self.wait_for(
|
||||||
|
lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_applet_my_posts")
|
||||||
|
)
|
||||||
|
invite_links = my_posts.find_elements(By.PARTIAL_LINK_TEXT, "INVITE:")
|
||||||
|
self.assertEqual(len(invite_links), 0, "Dismissed invite should not appear in My Posts")
|
||||||
|
|
||||||
|
def test_ok_with_valid_deck_shows_confirm_guard(self):
|
||||||
|
"""With a valid deck equipped, OK shows a Log-Out-style confirm guard."""
|
||||||
|
invitee, post, room = self._make_invite_post(equip_deck=True)
|
||||||
|
session_key = self.create_pre_authenticated_session(INVITEE_EMAIL)
|
||||||
|
self.browser.get(self.live_server_url)
|
||||||
|
self.browser.add_cookie({"name": "sessionid", "value": session_key})
|
||||||
|
self.browser.get(self.live_server_url + f"/billboard/post/{post.pk}/")
|
||||||
|
|
||||||
|
ok_btn = self.wait_for(
|
||||||
|
lambda: self.browser.find_element(By.CSS_SELECTOR, ".btn-confirm")
|
||||||
|
)
|
||||||
|
self.assertNotIn("btn-disabled", ok_btn.get_attribute("class"))
|
||||||
|
ok_btn.click()
|
||||||
|
|
||||||
|
# Confirm guard appears ("Join game?" with OK / NVM)
|
||||||
|
guard = self.wait_for(
|
||||||
|
lambda: self.browser.find_element(By.CSS_SELECTOR, ".join-guard")
|
||||||
|
)
|
||||||
|
self.assertIn("JOIN", guard.text.upper())
|
||||||
|
self.wait_for(lambda: guard.find_element(By.CSS_SELECTOR, ".btn-confirm"))
|
||||||
|
self.wait_for(lambda: guard.find_element(By.CSS_SELECTOR, ".btn-nvm"))
|
||||||
|
|
||||||
|
|
||||||
|
# ── Sprint 5 ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class GameInviteDeckValidationTest(FunctionalTest):
|
||||||
|
"""Sprint 5: OK deck validation — disabled without valid deck; on join, deck locked."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.browser.set_window_size(800, 1200)
|
||||||
|
_applets()
|
||||||
|
for slug, name, ctx in [
|
||||||
|
("game-kit", "Game Kit", "gameboard"),
|
||||||
|
("gk-decks", "Card Decks","game-kit"),
|
||||||
|
]:
|
||||||
|
Applet.objects.get_or_create(slug=slug, defaults={"name": name, "context": ctx})
|
||||||
|
|
||||||
|
def _make_invite_post(self, equip_deck=False, deck_in_use=False):
|
||||||
|
room, founder = _make_open_room()
|
||||||
|
invitee, _ = User.objects.get_or_create(email=INVITEE_EMAIL)
|
||||||
|
invite = RoomInvite.objects.create(
|
||||||
|
room=room, inviter=founder, invitee_email=INVITEE_EMAIL,
|
||||||
|
status=RoomInvite.PENDING,
|
||||||
|
)
|
||||||
|
post = BillPost.objects.create(
|
||||||
|
kind=BillPost.INVITE, recipient=invitee, room=room, invite=invite,
|
||||||
|
)
|
||||||
|
earthman = DeckVariant.objects.get(slug="earthman")
|
||||||
|
if equip_deck or deck_in_use:
|
||||||
|
invitee.equipped_deck = earthman
|
||||||
|
invitee.unlocked_decks.add(earthman)
|
||||||
|
invitee.save(update_fields=["equipped_deck"])
|
||||||
|
if deck_in_use:
|
||||||
|
# Simulate the deck already being assigned to another active seat
|
||||||
|
other_room = Room.objects.create(name="Other Game", owner=invitee)
|
||||||
|
TableSeat.objects.create(
|
||||||
|
gamer=invitee, room=other_room, role="PC", deck_variant=earthman,
|
||||||
|
)
|
||||||
|
return invitee, post, room, earthman
|
||||||
|
|
||||||
|
def test_ok_immediately_disabled_without_equipped_deck(self):
|
||||||
|
"""OK is btn-disabled on load when invitee has no deck equipped."""
|
||||||
|
invitee, post, room, _ = self._make_invite_post(equip_deck=False)
|
||||||
|
session_key = self.create_pre_authenticated_session(INVITEE_EMAIL)
|
||||||
|
self.browser.get(self.live_server_url)
|
||||||
|
self.browser.add_cookie({"name": "sessionid", "value": session_key})
|
||||||
|
self.browser.get(self.live_server_url + f"/billboard/post/{post.pk}/")
|
||||||
|
|
||||||
|
ok_btn = self.wait_for(
|
||||||
|
lambda: self.browser.find_element(By.CSS_SELECTOR, ".btn-confirm")
|
||||||
|
)
|
||||||
|
self.assertIn("btn-disabled", ok_btn.get_attribute("class"),
|
||||||
|
"OK should be disabled when invitee has no deck equipped")
|
||||||
|
|
||||||
|
# Tooltip explains the situation
|
||||||
|
ok_btn.click()
|
||||||
|
tooltip = self.wait_for(
|
||||||
|
lambda: self.browser.find_element(By.CSS_SELECTOR, ".invite-no-deck-tooltip")
|
||||||
|
)
|
||||||
|
self.assertTrue(tooltip.is_displayed())
|
||||||
|
|
||||||
|
def test_ok_immediately_disabled_when_deck_in_use(self):
|
||||||
|
"""OK is btn-disabled when invitee's equipped deck is already assigned to another game."""
|
||||||
|
invitee, post, room, _ = self._make_invite_post(deck_in_use=True)
|
||||||
|
session_key = self.create_pre_authenticated_session(INVITEE_EMAIL)
|
||||||
|
self.browser.get(self.live_server_url)
|
||||||
|
self.browser.add_cookie({"name": "sessionid", "value": session_key})
|
||||||
|
self.browser.get(self.live_server_url + f"/billboard/post/{post.pk}/")
|
||||||
|
|
||||||
|
ok_btn = self.wait_for(
|
||||||
|
lambda: self.browser.find_element(By.CSS_SELECTOR, ".btn-confirm")
|
||||||
|
)
|
||||||
|
self.assertIn("btn-disabled", ok_btn.get_attribute("class"),
|
||||||
|
"OK should be disabled when invitee's deck is already in use")
|
||||||
|
|
||||||
|
def test_confirming_join_assigns_deck_to_seat_and_locks_game_kit(self):
|
||||||
|
"""After confirming join, invitee's deck is assigned to their new seat and
|
||||||
|
the Game Kit DON button for that deck becomes btn-disabled."""
|
||||||
|
invitee, post, room, earthman = self._make_invite_post(equip_deck=True)
|
||||||
|
session_key = self.create_pre_authenticated_session(INVITEE_EMAIL)
|
||||||
|
self.browser.get(self.live_server_url)
|
||||||
|
self.browser.add_cookie({"name": "sessionid", "value": session_key})
|
||||||
|
self.browser.get(self.live_server_url + f"/billboard/post/{post.pk}/")
|
||||||
|
|
||||||
|
# OK → guard → confirm
|
||||||
|
ok_btn = self.wait_for(
|
||||||
|
lambda: self.browser.find_element(By.CSS_SELECTOR, ".btn-confirm")
|
||||||
|
)
|
||||||
|
ok_btn.click()
|
||||||
|
guard_ok = self.wait_for(
|
||||||
|
lambda: self.browser.find_element(By.CSS_SELECTOR, ".join-guard .btn-confirm")
|
||||||
|
)
|
||||||
|
guard_ok.click()
|
||||||
|
|
||||||
|
# Redirected into the room
|
||||||
|
self.wait_for(
|
||||||
|
lambda: self.assertIn(f"/gameboard/room/{room.pk}/", self.browser.current_url)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Invitee's seat has their deck assigned
|
||||||
|
self.wait_for(lambda: self.assertTrue(
|
||||||
|
TableSeat.objects.filter(
|
||||||
|
gamer=invitee, room=room, deck_variant=earthman,
|
||||||
|
).exists(),
|
||||||
|
"TableSeat.deck_variant was not set on join",
|
||||||
|
))
|
||||||
|
|
||||||
|
# Game Kit reflects in-use state
|
||||||
|
self.browser.get(self.live_server_url + "/gameboard/game-kit/")
|
||||||
|
self.wait_for(
|
||||||
|
lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_kit_card_deck")
|
||||||
|
).click()
|
||||||
|
self.wait_for(
|
||||||
|
lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_kit_earthman_deck")
|
||||||
|
).click()
|
||||||
|
don_btn = self.wait_for(
|
||||||
|
lambda: self.browser.find_element(
|
||||||
|
By.CSS_SELECTOR, "#id_kit_earthman_deck .btn-equip"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertIn("btn-disabled", don_btn.get_attribute("class"))
|
||||||
Reference in New Issue
Block a user