Files
python-tdd/src/functional_tests/test_room_sea_select.py

356 lines
15 KiB
Python
Raw Normal View History

"""Functional tests for the PICK SEA overlay — Celtic Cross draw."""
from django.test import tag
from django.urls import reverse
from django.utils import timezone
from selenium.webdriver.common.by import By
from apps.applets.models import Applet
from apps.epic.models import Character, GateSlot, Room, TableSeat, TarotCard, DeckVariant
from apps.lyric.models import User
from .base import ChannelsFunctionalTest
def _make_sky_confirmed_room(live_server_url, user, earthman):
"""Create a SKY_SELECT room with one gamer seated and sig assigned.
Returns (room, seat). The Character is NOT yet confirmed call
_confirm_sky() in the browser to trigger the async transition.
"""
room = Room.objects.create(
name="Sea Test Room", table_status=Room.SKY_SELECT, owner=user
)
slot = room.gate_slots.get(slot_number=1)
slot.gamer = user
slot.status = GateSlot.FILLED
slot.save()
room.gate_status = Room.OPEN
room.save()
sig_card = TarotCard.objects.filter(deck_variant=earthman, arcana="MAJOR").first()
seat = TableSeat.objects.create(
room=room, gamer=user, role="PC", slot_number=1,
deck_variant=earthman, significator=sig_card,
)
return room, seat
@tag("channels")
class PickSeaAsyncTransitionTest(ChannelsFunctionalTest):
SAVE SKY provenance + sky→hex (not sky→sea) transition — TDD - drama.GameEvent.SKY_SAVED verb + to_prose branch: "X beholds the skyscape of {poss} birth, which yields {obj} a unique {Cap} capacity."; tied highest scores switch "a unique" → "equal", join w. "and" (2-way) or Oxford comma (3+), and pluralize "capacity" → "capacities"; pronouns resolved from actor.pronouns at render time, same machinery as SIG_READY/ROLE_SELECTED - epic.utils.ELEMENT_CAPACITOR_NAMES + ELEMENT_ORDER + top_capacitors(elements) helper: maps Fire→Ardor Stone→Ossum Time→Tempo Space→Nexus Air→Pneuma Water→Humor; tolerates both flat-int and enriched-dict (`{count, contributors}`) chart_data shapes; returns capacitor names tied for highest count, ordered by canonical wheel ring - epic.natus_save: on action=confirm, records GameEvent.SKY_SAVED w. top_capacitors=[…] before _notify_sky_confirmed; per-room billscroll AND billboard Most Recent Scroll pick up the new prose - _natus_overlay.html _onSkyConfirmed: removed sea-partial fetch+inject; now calls closeNatus() + window.location.reload() so the gamer lands on the table hex w. the PICK SKY → PICK SEA btn swap (server-side, driven by sky_confirmed=True), then opts into the sea overlay manually. The auto-launch via 39e12d6 was buried by FTs that were pinning the wrong contract — gamer never had a chance to witness PICK SEA on the hex - test_room_sea_select.py: three FTs renamed/rewired from auto-launch assertions (sea_overlay_appears_without_page_refresh, natus_overlay_not_visible_after_sky_confirm, sea_open_class_on_html_after_confirm) to (pick_sea_btn_visible_after_sky_confirm, natus_overlay_closed_after_sky_confirm, clicking_pick_sea_btn_opens_sea_overlay) — sea overlay now requires explicit PICK SEA click Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 01:57:35 -04:00
"""After sky confirm, the natus overlay closes and the room reloads to the
table hex w. the PICK SEA btn visible the gamer must opt into the sea
overlay rather than be auto-launched into it."""
def setUp(self):
super().setUp()
self.browser.set_window_size(800, 1200)
Applet.objects.get_or_create(
slug="new-game", defaults={"name": "New Game", "context": "gameboard"}
)
Applet.objects.get_or_create(
slug="my-games", defaults={"name": "My Games", "context": "gameboard"}
)
from apps.lyric.models import User
gamer, _ = User.objects.get_or_create(email="founder@test.io")
earthman, _ = DeckVariant.objects.get_or_create(
slug="earthman",
defaults={"name": "Earthman Deck", "card_count": 108, "is_default": True},
)
gamer.unlocked_decks.add(earthman)
gamer.equipped_deck = earthman
gamer.save(update_fields=["equipped_deck"])
self.gamer = gamer
self.room, self.seat = _make_sky_confirmed_room(self.live_server_url, gamer, earthman)
self.room_url = self.live_server_url + reverse(
"epic:room", kwargs={"room_id": self.room.id}
)
self.natus_save_url = self.live_server_url + reverse(
"epic:natus_save", kwargs={"room_id": self.room.id}
)
def _confirm_sky(self):
"""POST to natus_save with action=confirm from browser JS (bypasses chart form)."""
# Wait for the room WS connection to be ready before triggering confirm
self.wait_for(lambda: self.browser.execute_script(
"return !!(window._roomSocket && window._roomSocket.readyState === 1);"
))
self.browser.execute_script(f"""
const csrf = (document.cookie.match(/csrftoken=([^;]+)/) || ['',''])[1];
fetch('{self.natus_save_url}', {{
method: 'POST',
credentials: 'same-origin',
headers: {{'Content-Type': 'application/json', 'X-CSRFToken': csrf}},
body: JSON.stringify({{
birth_dt: '1990-06-15T09:00:00Z',
birth_lat: 51.5, birth_lon: -0.1,
birth_place: 'London', house_system: 'O',
chart_data: {{}}, action: 'confirm',
}}),
}});
""")
SAVE SKY provenance + sky→hex (not sky→sea) transition — TDD - drama.GameEvent.SKY_SAVED verb + to_prose branch: "X beholds the skyscape of {poss} birth, which yields {obj} a unique {Cap} capacity."; tied highest scores switch "a unique" → "equal", join w. "and" (2-way) or Oxford comma (3+), and pluralize "capacity" → "capacities"; pronouns resolved from actor.pronouns at render time, same machinery as SIG_READY/ROLE_SELECTED - epic.utils.ELEMENT_CAPACITOR_NAMES + ELEMENT_ORDER + top_capacitors(elements) helper: maps Fire→Ardor Stone→Ossum Time→Tempo Space→Nexus Air→Pneuma Water→Humor; tolerates both flat-int and enriched-dict (`{count, contributors}`) chart_data shapes; returns capacitor names tied for highest count, ordered by canonical wheel ring - epic.natus_save: on action=confirm, records GameEvent.SKY_SAVED w. top_capacitors=[…] before _notify_sky_confirmed; per-room billscroll AND billboard Most Recent Scroll pick up the new prose - _natus_overlay.html _onSkyConfirmed: removed sea-partial fetch+inject; now calls closeNatus() + window.location.reload() so the gamer lands on the table hex w. the PICK SKY → PICK SEA btn swap (server-side, driven by sky_confirmed=True), then opts into the sea overlay manually. The auto-launch via 39e12d6 was buried by FTs that were pinning the wrong contract — gamer never had a chance to witness PICK SEA on the hex - test_room_sea_select.py: three FTs renamed/rewired from auto-launch assertions (sea_overlay_appears_without_page_refresh, natus_overlay_not_visible_after_sky_confirm, sea_open_class_on_html_after_confirm) to (pick_sea_btn_visible_after_sky_confirm, natus_overlay_closed_after_sky_confirm, clicking_pick_sea_btn_opens_sea_overlay) — sea overlay now requires explicit PICK SEA click Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 01:57:35 -04:00
def test_pick_sea_btn_visible_after_sky_confirm(self):
"""Confirming sky reloads the room to the hex w. PICK SEA replacing
PICK SKY; the sea overlay is NOT auto-opened."""
self.create_pre_authenticated_session("founder@test.io")
self.browser.get(self.room_url)
SAVE SKY provenance + sky→hex (not sky→sea) transition — TDD - drama.GameEvent.SKY_SAVED verb + to_prose branch: "X beholds the skyscape of {poss} birth, which yields {obj} a unique {Cap} capacity."; tied highest scores switch "a unique" → "equal", join w. "and" (2-way) or Oxford comma (3+), and pluralize "capacity" → "capacities"; pronouns resolved from actor.pronouns at render time, same machinery as SIG_READY/ROLE_SELECTED - epic.utils.ELEMENT_CAPACITOR_NAMES + ELEMENT_ORDER + top_capacitors(elements) helper: maps Fire→Ardor Stone→Ossum Time→Tempo Space→Nexus Air→Pneuma Water→Humor; tolerates both flat-int and enriched-dict (`{count, contributors}`) chart_data shapes; returns capacitor names tied for highest count, ordered by canonical wheel ring - epic.natus_save: on action=confirm, records GameEvent.SKY_SAVED w. top_capacitors=[…] before _notify_sky_confirmed; per-room billscroll AND billboard Most Recent Scroll pick up the new prose - _natus_overlay.html _onSkyConfirmed: removed sea-partial fetch+inject; now calls closeNatus() + window.location.reload() so the gamer lands on the table hex w. the PICK SKY → PICK SEA btn swap (server-side, driven by sky_confirmed=True), then opts into the sea overlay manually. The auto-launch via 39e12d6 was buried by FTs that were pinning the wrong contract — gamer never had a chance to witness PICK SEA on the hex - test_room_sea_select.py: three FTs renamed/rewired from auto-launch assertions (sea_overlay_appears_without_page_refresh, natus_overlay_not_visible_after_sky_confirm, sea_open_class_on_html_after_confirm) to (pick_sea_btn_visible_after_sky_confirm, natus_overlay_closed_after_sky_confirm, clicking_pick_sea_btn_opens_sea_overlay) — sea overlay now requires explicit PICK SEA click Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 01:57:35 -04:00
# Sky not yet confirmed — PICK SKY btn present.
self.wait_for(lambda: self.browser.find_element(By.ID, "id_pick_sky_btn"))
self._confirm_sky()
SAVE SKY provenance + sky→hex (not sky→sea) transition — TDD - drama.GameEvent.SKY_SAVED verb + to_prose branch: "X beholds the skyscape of {poss} birth, which yields {obj} a unique {Cap} capacity."; tied highest scores switch "a unique" → "equal", join w. "and" (2-way) or Oxford comma (3+), and pluralize "capacity" → "capacities"; pronouns resolved from actor.pronouns at render time, same machinery as SIG_READY/ROLE_SELECTED - epic.utils.ELEMENT_CAPACITOR_NAMES + ELEMENT_ORDER + top_capacitors(elements) helper: maps Fire→Ardor Stone→Ossum Time→Tempo Space→Nexus Air→Pneuma Water→Humor; tolerates both flat-int and enriched-dict (`{count, contributors}`) chart_data shapes; returns capacitor names tied for highest count, ordered by canonical wheel ring - epic.natus_save: on action=confirm, records GameEvent.SKY_SAVED w. top_capacitors=[…] before _notify_sky_confirmed; per-room billscroll AND billboard Most Recent Scroll pick up the new prose - _natus_overlay.html _onSkyConfirmed: removed sea-partial fetch+inject; now calls closeNatus() + window.location.reload() so the gamer lands on the table hex w. the PICK SKY → PICK SEA btn swap (server-side, driven by sky_confirmed=True), then opts into the sea overlay manually. The auto-launch via 39e12d6 was buried by FTs that were pinning the wrong contract — gamer never had a chance to witness PICK SEA on the hex - test_room_sea_select.py: three FTs renamed/rewired from auto-launch assertions (sea_overlay_appears_without_page_refresh, natus_overlay_not_visible_after_sky_confirm, sea_open_class_on_html_after_confirm) to (pick_sea_btn_visible_after_sky_confirm, natus_overlay_closed_after_sky_confirm, clicking_pick_sea_btn_opens_sea_overlay) — sea overlay now requires explicit PICK SEA click Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 01:57:35 -04:00
# Page reloads → hex shows PICK SEA in place of PICK SKY.
self.wait_for(lambda: self.browser.find_element(By.ID, "id_pick_sea_btn"))
self.assertEqual(self.browser.find_elements(By.ID, "id_pick_sky_btn"), [])
# Sea overlay is NOT auto-opened — it only appears once the gamer
# clicks PICK SEA.
has_sea_open = self.browser.execute_script(
"return document.documentElement.classList.contains('sea-open');"
)
SAVE SKY provenance + sky→hex (not sky→sea) transition — TDD - drama.GameEvent.SKY_SAVED verb + to_prose branch: "X beholds the skyscape of {poss} birth, which yields {obj} a unique {Cap} capacity."; tied highest scores switch "a unique" → "equal", join w. "and" (2-way) or Oxford comma (3+), and pluralize "capacity" → "capacities"; pronouns resolved from actor.pronouns at render time, same machinery as SIG_READY/ROLE_SELECTED - epic.utils.ELEMENT_CAPACITOR_NAMES + ELEMENT_ORDER + top_capacitors(elements) helper: maps Fire→Ardor Stone→Ossum Time→Tempo Space→Nexus Air→Pneuma Water→Humor; tolerates both flat-int and enriched-dict (`{count, contributors}`) chart_data shapes; returns capacitor names tied for highest count, ordered by canonical wheel ring - epic.natus_save: on action=confirm, records GameEvent.SKY_SAVED w. top_capacitors=[…] before _notify_sky_confirmed; per-room billscroll AND billboard Most Recent Scroll pick up the new prose - _natus_overlay.html _onSkyConfirmed: removed sea-partial fetch+inject; now calls closeNatus() + window.location.reload() so the gamer lands on the table hex w. the PICK SKY → PICK SEA btn swap (server-side, driven by sky_confirmed=True), then opts into the sea overlay manually. The auto-launch via 39e12d6 was buried by FTs that were pinning the wrong contract — gamer never had a chance to witness PICK SEA on the hex - test_room_sea_select.py: three FTs renamed/rewired from auto-launch assertions (sea_overlay_appears_without_page_refresh, natus_overlay_not_visible_after_sky_confirm, sea_open_class_on_html_after_confirm) to (pick_sea_btn_visible_after_sky_confirm, natus_overlay_closed_after_sky_confirm, clicking_pick_sea_btn_opens_sea_overlay) — sea overlay now requires explicit PICK SEA click Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 01:57:35 -04:00
self.assertFalse(has_sea_open)
SAVE SKY provenance + sky→hex (not sky→sea) transition — TDD - drama.GameEvent.SKY_SAVED verb + to_prose branch: "X beholds the skyscape of {poss} birth, which yields {obj} a unique {Cap} capacity."; tied highest scores switch "a unique" → "equal", join w. "and" (2-way) or Oxford comma (3+), and pluralize "capacity" → "capacities"; pronouns resolved from actor.pronouns at render time, same machinery as SIG_READY/ROLE_SELECTED - epic.utils.ELEMENT_CAPACITOR_NAMES + ELEMENT_ORDER + top_capacitors(elements) helper: maps Fire→Ardor Stone→Ossum Time→Tempo Space→Nexus Air→Pneuma Water→Humor; tolerates both flat-int and enriched-dict (`{count, contributors}`) chart_data shapes; returns capacitor names tied for highest count, ordered by canonical wheel ring - epic.natus_save: on action=confirm, records GameEvent.SKY_SAVED w. top_capacitors=[…] before _notify_sky_confirmed; per-room billscroll AND billboard Most Recent Scroll pick up the new prose - _natus_overlay.html _onSkyConfirmed: removed sea-partial fetch+inject; now calls closeNatus() + window.location.reload() so the gamer lands on the table hex w. the PICK SKY → PICK SEA btn swap (server-side, driven by sky_confirmed=True), then opts into the sea overlay manually. The auto-launch via 39e12d6 was buried by FTs that were pinning the wrong contract — gamer never had a chance to witness PICK SEA on the hex - test_room_sea_select.py: three FTs renamed/rewired from auto-launch assertions (sea_overlay_appears_without_page_refresh, natus_overlay_not_visible_after_sky_confirm, sea_open_class_on_html_after_confirm) to (pick_sea_btn_visible_after_sky_confirm, natus_overlay_closed_after_sky_confirm, clicking_pick_sea_btn_opens_sea_overlay) — sea overlay now requires explicit PICK SEA click Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 01:57:35 -04:00
def test_natus_overlay_closed_after_sky_confirm(self):
"""Natus overlay is gone (page reloaded) after sky confirm."""
self.create_pre_authenticated_session("founder@test.io")
self.browser.get(self.room_url)
self.wait_for(lambda: self.browser.find_element(By.ID, "id_pick_sky_btn"))
self._confirm_sky()
SAVE SKY provenance + sky→hex (not sky→sea) transition — TDD - drama.GameEvent.SKY_SAVED verb + to_prose branch: "X beholds the skyscape of {poss} birth, which yields {obj} a unique {Cap} capacity."; tied highest scores switch "a unique" → "equal", join w. "and" (2-way) or Oxford comma (3+), and pluralize "capacity" → "capacities"; pronouns resolved from actor.pronouns at render time, same machinery as SIG_READY/ROLE_SELECTED - epic.utils.ELEMENT_CAPACITOR_NAMES + ELEMENT_ORDER + top_capacitors(elements) helper: maps Fire→Ardor Stone→Ossum Time→Tempo Space→Nexus Air→Pneuma Water→Humor; tolerates both flat-int and enriched-dict (`{count, contributors}`) chart_data shapes; returns capacitor names tied for highest count, ordered by canonical wheel ring - epic.natus_save: on action=confirm, records GameEvent.SKY_SAVED w. top_capacitors=[…] before _notify_sky_confirmed; per-room billscroll AND billboard Most Recent Scroll pick up the new prose - _natus_overlay.html _onSkyConfirmed: removed sea-partial fetch+inject; now calls closeNatus() + window.location.reload() so the gamer lands on the table hex w. the PICK SKY → PICK SEA btn swap (server-side, driven by sky_confirmed=True), then opts into the sea overlay manually. The auto-launch via 39e12d6 was buried by FTs that were pinning the wrong contract — gamer never had a chance to witness PICK SEA on the hex - test_room_sea_select.py: three FTs renamed/rewired from auto-launch assertions (sea_overlay_appears_without_page_refresh, natus_overlay_not_visible_after_sky_confirm, sea_open_class_on_html_after_confirm) to (pick_sea_btn_visible_after_sky_confirm, natus_overlay_closed_after_sky_confirm, clicking_pick_sea_btn_opens_sea_overlay) — sea overlay now requires explicit PICK SEA click Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 01:57:35 -04:00
self.wait_for(lambda: self.browser.find_element(By.ID, "id_pick_sea_btn"))
natus = self.browser.find_elements(By.ID, "id_natus_overlay")
self.assertTrue(not natus or not natus[0].is_displayed())
SAVE SKY provenance + sky→hex (not sky→sea) transition — TDD - drama.GameEvent.SKY_SAVED verb + to_prose branch: "X beholds the skyscape of {poss} birth, which yields {obj} a unique {Cap} capacity."; tied highest scores switch "a unique" → "equal", join w. "and" (2-way) or Oxford comma (3+), and pluralize "capacity" → "capacities"; pronouns resolved from actor.pronouns at render time, same machinery as SIG_READY/ROLE_SELECTED - epic.utils.ELEMENT_CAPACITOR_NAMES + ELEMENT_ORDER + top_capacitors(elements) helper: maps Fire→Ardor Stone→Ossum Time→Tempo Space→Nexus Air→Pneuma Water→Humor; tolerates both flat-int and enriched-dict (`{count, contributors}`) chart_data shapes; returns capacitor names tied for highest count, ordered by canonical wheel ring - epic.natus_save: on action=confirm, records GameEvent.SKY_SAVED w. top_capacitors=[…] before _notify_sky_confirmed; per-room billscroll AND billboard Most Recent Scroll pick up the new prose - _natus_overlay.html _onSkyConfirmed: removed sea-partial fetch+inject; now calls closeNatus() + window.location.reload() so the gamer lands on the table hex w. the PICK SKY → PICK SEA btn swap (server-side, driven by sky_confirmed=True), then opts into the sea overlay manually. The auto-launch via 39e12d6 was buried by FTs that were pinning the wrong contract — gamer never had a chance to witness PICK SEA on the hex - test_room_sea_select.py: three FTs renamed/rewired from auto-launch assertions (sea_overlay_appears_without_page_refresh, natus_overlay_not_visible_after_sky_confirm, sea_open_class_on_html_after_confirm) to (pick_sea_btn_visible_after_sky_confirm, natus_overlay_closed_after_sky_confirm, clicking_pick_sea_btn_opens_sea_overlay) — sea overlay now requires explicit PICK SEA click Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 01:57:35 -04:00
def test_clicking_pick_sea_btn_opens_sea_overlay(self):
"""The gamer's explicit click on PICK SEA is what opens the sea overlay."""
self.create_pre_authenticated_session("founder@test.io")
self.browser.get(self.room_url)
self.wait_for(lambda: self.browser.find_element(By.ID, "id_pick_sky_btn"))
self._confirm_sky()
self.wait_for(lambda: self.browser.find_element(By.ID, "id_pick_sea_btn"))
# On slow CI, the PICK SEA btn parses into the DOM before the inline
# `<script>` at the bottom of _sea_overlay.html has bound `openSea` to
# it; a one-shot click can land before the handler exists. Retry click
# + assert together via wait_for so the race resolves naturally.
def _click_and_assert_sea_open():
btn = self.browser.find_element(By.ID, "id_pick_sea_btn")
self.browser.execute_script("arguments[0].click()", btn)
self.assertTrue(self.browser.execute_script(
"return document.documentElement.classList.contains('sea-open')"
))
self.wait_for(_click_and_assert_sea_open)
sea_overlay = self.browser.find_element(By.ID, "id_sea_overlay")
SAVE SKY provenance + sky→hex (not sky→sea) transition — TDD - drama.GameEvent.SKY_SAVED verb + to_prose branch: "X beholds the skyscape of {poss} birth, which yields {obj} a unique {Cap} capacity."; tied highest scores switch "a unique" → "equal", join w. "and" (2-way) or Oxford comma (3+), and pluralize "capacity" → "capacities"; pronouns resolved from actor.pronouns at render time, same machinery as SIG_READY/ROLE_SELECTED - epic.utils.ELEMENT_CAPACITOR_NAMES + ELEMENT_ORDER + top_capacitors(elements) helper: maps Fire→Ardor Stone→Ossum Time→Tempo Space→Nexus Air→Pneuma Water→Humor; tolerates both flat-int and enriched-dict (`{count, contributors}`) chart_data shapes; returns capacitor names tied for highest count, ordered by canonical wheel ring - epic.natus_save: on action=confirm, records GameEvent.SKY_SAVED w. top_capacitors=[…] before _notify_sky_confirmed; per-room billscroll AND billboard Most Recent Scroll pick up the new prose - _natus_overlay.html _onSkyConfirmed: removed sea-partial fetch+inject; now calls closeNatus() + window.location.reload() so the gamer lands on the table hex w. the PICK SKY → PICK SEA btn swap (server-side, driven by sky_confirmed=True), then opts into the sea overlay manually. The auto-launch via 39e12d6 was buried by FTs that were pinning the wrong contract — gamer never had a chance to witness PICK SEA on the hex - test_room_sea_select.py: three FTs renamed/rewired from auto-launch assertions (sea_overlay_appears_without_page_refresh, natus_overlay_not_visible_after_sky_confirm, sea_open_class_on_html_after_confirm) to (pick_sea_btn_visible_after_sky_confirm, natus_overlay_closed_after_sky_confirm, clicking_pick_sea_btn_opens_sea_overlay) — sea overlay now requires explicit PICK SEA click Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 01:57:35 -04:00
self.assertTrue(sea_overlay.is_displayed())
# ── Helpers for PICK SEA deal tests ──────────────────────────────────────────
def _seed_earthman_cards(earthman, count=20):
"""Seed enough Middle Arcana cards for the deck piles."""
suits = ["BRANDS", "GRAILS", "BLADES", "CROWNS"]
nums = [11, 12, 13, 14]
for suit in suits:
for num in nums:
TarotCard.objects.get_or_create(
deck_variant=earthman,
slug=f"m{num}-{suit.lower()}-em",
defaults={"arcana": "MIDDLE", "suit": suit, "number": num,
"name": f"Card {num} {suit}"},
)
def _make_sea_ready_room(earthman):
"""Create a SKY_SELECT room with a confirmed Character ready for PICK SEA.
Returns (room, gamer, seat, char, room_url).
"""
gamer, _ = User.objects.get_or_create(email="founder@test.io")
gamer.unlocked_decks.add(earthman)
gamer.equipped_deck = earthman
gamer.save(update_fields=["equipped_deck"])
room = Room.objects.create(
name="Sea Deal Room", table_status=Room.SKY_SELECT, owner=gamer
)
slot = room.gate_slots.get(slot_number=1)
slot.gamer = gamer
slot.status = GateSlot.FILLED
slot.save()
room.gate_status = Room.OPEN
room.save()
sig_card = TarotCard.objects.filter(deck_variant=earthman, arcana="MAJOR").first()
seat = TableSeat.objects.create(
room=room, gamer=gamer, role="PC", slot_number=1,
deck_variant=earthman, significator=sig_card,
)
char = Character.objects.create(
seat=seat, significator=sig_card, confirmed_at=timezone.now()
)
return room, gamer, seat, char
@tag("channels")
class PickSeaDealTest(ChannelsFunctionalTest):
"""PICK SEA deck stacks, OK btn interaction, card draw, and LOCK HAND."""
def setUp(self):
super().setUp()
self.browser.set_window_size(800, 1200)
Applet.objects.get_or_create(
slug="new-game", defaults={"name": "New Game", "context": "gameboard"}
)
Applet.objects.get_or_create(
slug="my-games", defaults={"name": "My Games", "context": "gameboard"}
)
# Major Arcana sig card
earthman, _ = DeckVariant.objects.get_or_create(
slug="earthman",
defaults={"name": "Earthman Deck", "card_count": 108, "is_default": True},
)
TarotCard.objects.get_or_create(
deck_variant=earthman, slug="the-schizo-em",
defaults={"arcana": "MAJOR", "number": 1, "name": "The Schizo",
"levity_qualifier": "Enlightened", "gravity_qualifier": "Engraven"},
)
_seed_earthman_cards(earthman)
self.room, self.gamer, self.seat, self.char = _make_sea_ready_room(earthman)
self.room_url = self.live_server_url + reverse(
"epic:room", kwargs={"room_id": self.room.id}
)
def _load_sea_overlay(self):
"""Navigate to room page and open the sea overlay."""
self.create_pre_authenticated_session("founder@test.io")
self.browser.get(self.room_url)
btn = self.wait_for(lambda: self.browser.find_element(By.ID, "id_pick_sea_btn"))
self.browser.execute_script("arguments[0].click()", btn)
self.wait_for(lambda: self.browser.find_element(By.ID, "id_sea_overlay"))
# ── Button presence ───────────────────────────────────────────────── #
def test_deal_btn_absent(self):
"""DEAL btn replaced by deck stacks + LOCK HAND."""
self._load_sea_overlay()
self.assertEqual(self.browser.find_elements(By.ID, "id_sea_deal"), [])
def test_lock_hand_btn_present_and_disabled(self):
"""LOCK HAND btn is present but disabled before any cards are drawn."""
self._load_sea_overlay()
lock_btn = self.wait_for(
lambda: self.browser.find_element(By.ID, "id_sea_lock_hand")
)
self.assertFalse(lock_btn.is_enabled())
def test_del_btn_present(self):
"""DEL btn is always present in the sea overlay."""
self._load_sea_overlay()
self.wait_for(lambda: self.browser.find_element(By.ID, "id_sea_del"))
# ── Deck stacks ───────────────────────────────────────────────────── #
def test_two_deck_stacks_present(self):
"""Both levity and gravity deck stacks are visible."""
self._load_sea_overlay()
self.wait_for(lambda: self.browser.find_element(
By.CSS_SELECTOR, ".sea-deck-stack--levity"
))
self.wait_for(lambda: self.browser.find_element(
By.CSS_SELECTOR, ".sea-deck-stack--gravity"
))
def test_clicking_stack_shows_ok_btn(self):
"""Clicking a deck stack reveals its OK btn."""
self._load_sea_overlay()
stack = self.wait_for(lambda: self.browser.find_element(
By.CSS_SELECTOR, ".sea-deck-stack--levity"
))
self.browser.execute_script("arguments[0].click()", stack)
ok_btn = self.wait_for(lambda: self.browser.find_element(
By.CSS_SELECTOR, ".sea-deck-stack--levity .sea-stack-ok"
))
self.assertTrue(ok_btn.is_displayed())
def test_clicking_elsewhere_hides_ok_btn(self):
"""Clicking outside a focused stack dismisses the OK btn."""
self._load_sea_overlay()
stack = self.wait_for(lambda: self.browser.find_element(
By.CSS_SELECTOR, ".sea-deck-stack--levity"
))
self.browser.execute_script("arguments[0].click()", stack)
self.wait_for(lambda: self.browser.find_element(
By.CSS_SELECTOR, ".sea-deck-stack--levity .sea-stack-ok"
).is_displayed())
# Click the sea cards column (not a stack)
col = self.browser.find_element(By.CSS_SELECTOR, ".sea-cards-col")
self.browser.execute_script("arguments[0].click()", col)
self.wait_for(lambda: not any(
el.is_displayed()
for el in self.browser.find_elements(By.CSS_SELECTOR, ".sea-stack-ok")
))
# ── Card draw ─────────────────────────────────────────────────────── #
def test_ok_click_fills_cover_position(self):
"""First OK click places a card in the Cover position."""
self._load_sea_overlay()
stack = self.wait_for(lambda: self.browser.find_element(
By.CSS_SELECTOR, ".sea-deck-stack--levity"
))
self.browser.execute_script("arguments[0].click()", stack)
ok_btn = self.wait_for(lambda: self.browser.find_element(
By.CSS_SELECTOR, ".sea-deck-stack--levity .sea-stack-ok"
))
self.browser.execute_script("arguments[0].click()", ok_btn)
self.wait_for(lambda: self.browser.find_element(
By.CSS_SELECTOR, ".sea-pos-cover .sea-card-slot--filled"
))
def test_lock_hand_enables_after_six_draws(self):
"""LOCK HAND btn becomes enabled once all 6 positions are filled."""
self._load_sea_overlay()
for _ in range(6):
stack = self.wait_for(lambda: self.browser.find_element(
By.CSS_SELECTOR, ".sea-deck-stack--levity"
))
self.browser.execute_script("arguments[0].click()", stack)
ok_btn = self.wait_for(lambda: self.browser.find_element(
By.CSS_SELECTOR, ".sea-deck-stack--levity .sea-stack-ok"
))
self.browser.execute_script("arguments[0].click()", ok_btn)
lock_btn = self.wait_for(
lambda: self.browser.find_element(By.ID, "id_sea_lock_hand")
)
self.assertTrue(lock_btn.is_enabled())
def test_del_clears_drawn_cards(self):
"""DEL btn clears all drawn cards and resets positions to empty."""
self._load_sea_overlay()
# Draw one card
stack = self.wait_for(lambda: self.browser.find_element(
By.CSS_SELECTOR, ".sea-deck-stack--levity"
))
self.browser.execute_script("arguments[0].click()", stack)
ok_btn = self.wait_for(lambda: self.browser.find_element(
By.CSS_SELECTOR, ".sea-deck-stack--levity .sea-stack-ok"
))
self.browser.execute_script("arguments[0].click()", ok_btn)
self.wait_for(lambda: self.browser.find_element(
By.CSS_SELECTOR, ".sea-pos-cover .sea-card-slot--filled"
))
# DEL clears it
del_btn = self.browser.find_element(By.ID, "id_sea_del")
self.browser.execute_script("arguments[0].click()", del_btn)
self.wait_for(lambda: not self.browser.find_elements(
By.CSS_SELECTOR, ".sea-card-slot--filled"
))