functional_tests + CI: rename pass + structural consolidations + parallel test-FTs split — every FT file now starts with one of 6 prefixes (test_admin_* / test_bill_* / test_core_* / test_dash_* / test_game_room_* / test_trinket_*) plus the 4 page-roots test_billboard / test_dashboard / test_gameboard / test_jasmine, so the partition is unambiguous and stable for tooling (the previous mix of test_applet_*, test_room_*, test_component_*, ad-hoc names had no consistent grouping); session-side: merged test_gatekeeper_bud_btn.py into test_bud_btn.py (then user renamed to test_core_bud_btn.py) — both files drove the same #id_bud_btn UI in two contexts (post-share + gatekeeper invite) and shared the bud-btn.js skeleton, so consolidation was overdue; split test_component_cards_tarot.py into test_admin_tarot.py (just TarotAdminTest, sitting next to test_admin / test_admin_post_readonly) + 3 classes (TarotDeckTest / GameKitDeckSelectionTest / GameKitPageTest) appended to test_game_room_tray.py; updated stale `test_bud_btn.py` references in the test_core_bud_btn.py docstring + test_admin_post_readonly.py comment to point at the new filename; user-driven renames (22 files): test_applet_my_notes/posts → test_bill_my_*, test_applet_new_post[_line_validation] → test_bill_new_post[_line_validation], test_applet_my_sky → test_dash_my_sky, test_applet_palette → test_dash_palette, test_wallet → test_dash_wallet, test_login → test_core_login, test_navbar → test_core_navbar, test_sharing → test_core_sharing, test_layout_and_styling → test_core_styling, test_my_buds → test_bill_my_buds, test_bud_btn → test_core_bud_btn, test_deck_contribution → test_game_room_deck_contrib, test_game_invite → test_game_room_invite, test_room_gatekeeper → test_game_room_gatekeeper, test_room_role_select → test_game_room_select_role, test_room_sea_select → test_game_room_select_sea, test_room_sig_select → test_game_room_select_sig, test_room_sky_select → test_game_room_select_sky, test_room_tray → test_game_room_tray, test_component_tray_tooltip → test_game_room_tray_tooltip; the post_page.py / room_page.py helper modules from the May-12 sprint absorbed the cross-file FT imports that would otherwise have cascade-broken on these renames
.woodpecker/main.yaml — CI test-FTs step splits into parallel siblings test-FTs-non-room (22 files via `ls functional_tests/test_*.py | grep -v 'test_game_room_'`) + test-FTs-room (9 files via `ls functional_tests/test_game_room_*.py`); room cluster is the heaviest (~70% of the pre-split ~40-min wall-clock) and now runs concurrently w. the rest instead of in series; DAG explicit via depends_on on every step (Woodpecker mixes default-sequential w. depends_on awkwardly, so each step pins its prerequisite); collectstatic stays in test-two-browser-FTs only — the shared workspace propagates assets to both parallel FT steps, no race + no duplication; screendumps + build-and-push fan back in (depends_on both parallel steps); deploy-staging + deploy-prod depend on build-and-push
smoke-import: 31/31 FT modules green after the rename pass
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 20:06:25 -04:00
|
|
|
|
import time
|
2026-03-28 18:52:46 -04:00
|
|
|
|
import unittest
|
|
|
|
|
|
|
functional_tests + CI: rename pass + structural consolidations + parallel test-FTs split — every FT file now starts with one of 6 prefixes (test_admin_* / test_bill_* / test_core_* / test_dash_* / test_game_room_* / test_trinket_*) plus the 4 page-roots test_billboard / test_dashboard / test_gameboard / test_jasmine, so the partition is unambiguous and stable for tooling (the previous mix of test_applet_*, test_room_*, test_component_*, ad-hoc names had no consistent grouping); session-side: merged test_gatekeeper_bud_btn.py into test_bud_btn.py (then user renamed to test_core_bud_btn.py) — both files drove the same #id_bud_btn UI in two contexts (post-share + gatekeeper invite) and shared the bud-btn.js skeleton, so consolidation was overdue; split test_component_cards_tarot.py into test_admin_tarot.py (just TarotAdminTest, sitting next to test_admin / test_admin_post_readonly) + 3 classes (TarotDeckTest / GameKitDeckSelectionTest / GameKitPageTest) appended to test_game_room_tray.py; updated stale `test_bud_btn.py` references in the test_core_bud_btn.py docstring + test_admin_post_readonly.py comment to point at the new filename; user-driven renames (22 files): test_applet_my_notes/posts → test_bill_my_*, test_applet_new_post[_line_validation] → test_bill_new_post[_line_validation], test_applet_my_sky → test_dash_my_sky, test_applet_palette → test_dash_palette, test_wallet → test_dash_wallet, test_login → test_core_login, test_navbar → test_core_navbar, test_sharing → test_core_sharing, test_layout_and_styling → test_core_styling, test_my_buds → test_bill_my_buds, test_bud_btn → test_core_bud_btn, test_deck_contribution → test_game_room_deck_contrib, test_game_invite → test_game_room_invite, test_room_gatekeeper → test_game_room_gatekeeper, test_room_role_select → test_game_room_select_role, test_room_sea_select → test_game_room_select_sea, test_room_sig_select → test_game_room_select_sig, test_room_sky_select → test_game_room_select_sky, test_room_tray → test_game_room_tray, test_component_tray_tooltip → test_game_room_tray_tooltip; the post_page.py / room_page.py helper modules from the May-12 sprint absorbed the cross-file FT imports that would otherwise have cascade-broken on these renames
.woodpecker/main.yaml — CI test-FTs step splits into parallel siblings test-FTs-non-room (22 files via `ls functional_tests/test_*.py | grep -v 'test_game_room_'`) + test-FTs-room (9 files via `ls functional_tests/test_game_room_*.py`); room cluster is the heaviest (~70% of the pre-split ~40-min wall-clock) and now runs concurrently w. the rest instead of in series; DAG explicit via depends_on on every step (Woodpecker mixes default-sequential w. depends_on awkwardly, so each step pins its prerequisite); collectstatic stays in test-two-browser-FTs only — the shared workspace propagates assets to both parallel FT steps, no race + no duplication; screendumps + build-and-push fan back in (depends_on both parallel steps); deploy-staging + deploy-prod depend on build-and-push
smoke-import: 31/31 FT modules green after the rename pass
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 20:06:25 -04:00
|
|
|
|
from django.test import tag
|
2026-03-24 21:07:01 -04:00
|
|
|
|
from selenium.webdriver.common.action_chains import ActionChains
|
|
|
|
|
|
from selenium.webdriver.common.by import By
|
|
|
|
|
|
|
|
|
|
|
|
from .base import FunctionalTest
|
functional_tests + CI: rename pass + structural consolidations + parallel test-FTs split — every FT file now starts with one of 6 prefixes (test_admin_* / test_bill_* / test_core_* / test_dash_* / test_game_room_* / test_trinket_*) plus the 4 page-roots test_billboard / test_dashboard / test_gameboard / test_jasmine, so the partition is unambiguous and stable for tooling (the previous mix of test_applet_*, test_room_*, test_component_*, ad-hoc names had no consistent grouping); session-side: merged test_gatekeeper_bud_btn.py into test_bud_btn.py (then user renamed to test_core_bud_btn.py) — both files drove the same #id_bud_btn UI in two contexts (post-share + gatekeeper invite) and shared the bud-btn.js skeleton, so consolidation was overdue; split test_component_cards_tarot.py into test_admin_tarot.py (just TarotAdminTest, sitting next to test_admin / test_admin_post_readonly) + 3 classes (TarotDeckTest / GameKitDeckSelectionTest / GameKitPageTest) appended to test_game_room_tray.py; updated stale `test_bud_btn.py` references in the test_core_bud_btn.py docstring + test_admin_post_readonly.py comment to point at the new filename; user-driven renames (22 files): test_applet_my_notes/posts → test_bill_my_*, test_applet_new_post[_line_validation] → test_bill_new_post[_line_validation], test_applet_my_sky → test_dash_my_sky, test_applet_palette → test_dash_palette, test_wallet → test_dash_wallet, test_login → test_core_login, test_navbar → test_core_navbar, test_sharing → test_core_sharing, test_layout_and_styling → test_core_styling, test_my_buds → test_bill_my_buds, test_bud_btn → test_core_bud_btn, test_deck_contribution → test_game_room_deck_contrib, test_game_invite → test_game_room_invite, test_room_gatekeeper → test_game_room_gatekeeper, test_room_role_select → test_game_room_select_role, test_room_sea_select → test_game_room_select_sea, test_room_sig_select → test_game_room_select_sig, test_room_sky_select → test_game_room_select_sky, test_room_tray → test_game_room_tray, test_component_tray_tooltip → test_game_room_tray_tooltip; the post_page.py / room_page.py helper modules from the May-12 sprint absorbed the cross-file FT imports that would otherwise have cascade-broken on these renames
.woodpecker/main.yaml — CI test-FTs step splits into parallel siblings test-FTs-non-room (22 files via `ls functional_tests/test_*.py | grep -v 'test_game_room_'`) + test-FTs-room (9 files via `ls functional_tests/test_game_room_*.py`); room cluster is the heaviest (~70% of the pre-split ~40-min wall-clock) and now runs concurrently w. the rest instead of in series; DAG explicit via depends_on on every step (Woodpecker mixes default-sequential w. depends_on awkwardly, so each step pins its prerequisite); collectstatic stays in test-two-browser-FTs only — the shared workspace propagates assets to both parallel FT steps, no race + no duplication; screendumps + build-and-push fan back in (depends_on both parallel steps); deploy-staging + deploy-prod depend on build-and-push
smoke-import: 31/31 FT modules green after the rename pass
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 20:06:25 -04:00
|
|
|
|
from .room_page import _assign_all_roles, _fill_room_via_orm
|
2026-03-24 21:07:01 -04:00
|
|
|
|
from apps.applets.models import Applet
|
|
|
|
|
|
from apps.epic.models import DeckVariant, Room
|
|
|
|
|
|
from apps.lyric.models import User
|
|
|
|
|
|
|
|
|
|
|
|
|
functional_tests + CI: rename pass + structural consolidations + parallel test-FTs split — every FT file now starts with one of 6 prefixes (test_admin_* / test_bill_* / test_core_* / test_dash_* / test_game_room_* / test_trinket_*) plus the 4 page-roots test_billboard / test_dashboard / test_gameboard / test_jasmine, so the partition is unambiguous and stable for tooling (the previous mix of test_applet_*, test_room_*, test_component_*, ad-hoc names had no consistent grouping); session-side: merged test_gatekeeper_bud_btn.py into test_bud_btn.py (then user renamed to test_core_bud_btn.py) — both files drove the same #id_bud_btn UI in two contexts (post-share + gatekeeper invite) and shared the bud-btn.js skeleton, so consolidation was overdue; split test_component_cards_tarot.py into test_admin_tarot.py (just TarotAdminTest, sitting next to test_admin / test_admin_post_readonly) + 3 classes (TarotDeckTest / GameKitDeckSelectionTest / GameKitPageTest) appended to test_game_room_tray.py; updated stale `test_bud_btn.py` references in the test_core_bud_btn.py docstring + test_admin_post_readonly.py comment to point at the new filename; user-driven renames (22 files): test_applet_my_notes/posts → test_bill_my_*, test_applet_new_post[_line_validation] → test_bill_new_post[_line_validation], test_applet_my_sky → test_dash_my_sky, test_applet_palette → test_dash_palette, test_wallet → test_dash_wallet, test_login → test_core_login, test_navbar → test_core_navbar, test_sharing → test_core_sharing, test_layout_and_styling → test_core_styling, test_my_buds → test_bill_my_buds, test_bud_btn → test_core_bud_btn, test_deck_contribution → test_game_room_deck_contrib, test_game_invite → test_game_room_invite, test_room_gatekeeper → test_game_room_gatekeeper, test_room_role_select → test_game_room_select_role, test_room_sea_select → test_game_room_select_sea, test_room_sig_select → test_game_room_select_sig, test_room_sky_select → test_game_room_select_sky, test_room_tray → test_game_room_tray, test_component_tray_tooltip → test_game_room_tray_tooltip; the post_page.py / room_page.py helper modules from the May-12 sprint absorbed the cross-file FT imports that would otherwise have cascade-broken on these renames
.woodpecker/main.yaml — CI test-FTs step splits into parallel siblings test-FTs-non-room (22 files via `ls functional_tests/test_*.py | grep -v 'test_game_room_'`) + test-FTs-room (9 files via `ls functional_tests/test_game_room_*.py`); room cluster is the heaviest (~70% of the pre-split ~40-min wall-clock) and now runs concurrently w. the rest instead of in series; DAG explicit via depends_on on every step (Woodpecker mixes default-sequential w. depends_on awkwardly, so each step pins its prerequisite); collectstatic stays in test-two-browser-FTs only — the shared workspace propagates assets to both parallel FT steps, no race + no duplication; screendumps + build-and-push fan back in (depends_on both parallel steps); deploy-staging + deploy-prod depend on build-and-push
smoke-import: 31/31 FT modules green after the rename pass
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 20:06:25 -04:00
|
|
|
|
# ── Seat Tray ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
#
|
|
|
|
|
|
# The Tray is a per-seat, per-room slide-out panel anchored to the right edge
|
|
|
|
|
|
# of the viewport. #id_tray_btn is a drawer-handle-shaped button: a circle
|
|
|
|
|
|
# with an icon (the "ivory centre") with decorative lines curving from its top
|
|
|
|
|
|
# and bottom to the right edge of the screen.
|
|
|
|
|
|
#
|
|
|
|
|
|
# Behaviour:
|
|
|
|
|
|
# - Closed by default; tray panel (#id_tray) is not visible.
|
|
|
|
|
|
# - Clicking the button while closed: wobbles the handle (adds "wobble"
|
|
|
|
|
|
# class) but does NOT open the tray.
|
|
|
|
|
|
# - Dragging the button leftward: reveals the tray.
|
|
|
|
|
|
# - Clicking the button while open: slides the tray closed.
|
|
|
|
|
|
# - On page reload: tray always starts closed (JS in-memory only).
|
|
|
|
|
|
#
|
|
|
|
|
|
# Contents (populated in later sprints): Role card, Significator, Celtic Cross
|
|
|
|
|
|
# draw, sky wheel, committed dice/cards for this table.
|
|
|
|
|
|
#
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TrayTest(FunctionalTest):
|
2026-03-24 21:07:01 -04:00
|
|
|
|
|
|
|
|
|
|
def setUp(self):
|
functional_tests + CI: rename pass + structural consolidations + parallel test-FTs split — every FT file now starts with one of 6 prefixes (test_admin_* / test_bill_* / test_core_* / test_dash_* / test_game_room_* / test_trinket_*) plus the 4 page-roots test_billboard / test_dashboard / test_gameboard / test_jasmine, so the partition is unambiguous and stable for tooling (the previous mix of test_applet_*, test_room_*, test_component_*, ad-hoc names had no consistent grouping); session-side: merged test_gatekeeper_bud_btn.py into test_bud_btn.py (then user renamed to test_core_bud_btn.py) — both files drove the same #id_bud_btn UI in two contexts (post-share + gatekeeper invite) and shared the bud-btn.js skeleton, so consolidation was overdue; split test_component_cards_tarot.py into test_admin_tarot.py (just TarotAdminTest, sitting next to test_admin / test_admin_post_readonly) + 3 classes (TarotDeckTest / GameKitDeckSelectionTest / GameKitPageTest) appended to test_game_room_tray.py; updated stale `test_bud_btn.py` references in the test_core_bud_btn.py docstring + test_admin_post_readonly.py comment to point at the new filename; user-driven renames (22 files): test_applet_my_notes/posts → test_bill_my_*, test_applet_new_post[_line_validation] → test_bill_new_post[_line_validation], test_applet_my_sky → test_dash_my_sky, test_applet_palette → test_dash_palette, test_wallet → test_dash_wallet, test_login → test_core_login, test_navbar → test_core_navbar, test_sharing → test_core_sharing, test_layout_and_styling → test_core_styling, test_my_buds → test_bill_my_buds, test_bud_btn → test_core_bud_btn, test_deck_contribution → test_game_room_deck_contrib, test_game_invite → test_game_room_invite, test_room_gatekeeper → test_game_room_gatekeeper, test_room_role_select → test_game_room_select_role, test_room_sea_select → test_game_room_select_sea, test_room_sig_select → test_game_room_select_sig, test_room_sky_select → test_game_room_select_sky, test_room_tray → test_game_room_tray, test_component_tray_tooltip → test_game_room_tray_tooltip; the post_page.py / room_page.py helper modules from the May-12 sprint absorbed the cross-file FT imports that would otherwise have cascade-broken on these renames
.woodpecker/main.yaml — CI test-FTs step splits into parallel siblings test-FTs-non-room (22 files via `ls functional_tests/test_*.py | grep -v 'test_game_room_'`) + test-FTs-room (9 files via `ls functional_tests/test_game_room_*.py`); room cluster is the heaviest (~70% of the pre-split ~40-min wall-clock) and now runs concurrently w. the rest instead of in series; DAG explicit via depends_on on every step (Woodpecker mixes default-sequential w. depends_on awkwardly, so each step pins its prerequisite); collectstatic stays in test-two-browser-FTs only — the shared workspace propagates assets to both parallel FT steps, no race + no duplication; screendumps + build-and-push fan back in (depends_on both parallel steps); deploy-staging + deploy-prod depend on build-and-push
smoke-import: 31/31 FT modules green after the rename pass
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 20:06:25 -04:00
|
|
|
|
# Portrait viewport for T1–T5 (768×1024). Use _make_browser so
|
|
|
|
|
|
# headless CI gets --width/--height args and the CSS orientation
|
|
|
|
|
|
# media query is correct from first paint.
|
|
|
|
|
|
self.browser = self._make_browser(768, 1024)
|
|
|
|
|
|
self.test_server = None
|
|
|
|
|
|
|
|
|
|
|
|
def _switch_to_landscape(self):
|
|
|
|
|
|
"""Recreate the browser, navigate to about:blank, then resize to
|
|
|
|
|
|
900×500 and wait until window.innerWidth > window.innerHeight confirms
|
|
|
|
|
|
the CSS orientation media query will fire correctly on the next page."""
|
|
|
|
|
|
self.browser.quit()
|
|
|
|
|
|
self.browser = self._make_browser(900, 500)
|
|
|
|
|
|
self.browser.get('about:blank')
|
|
|
|
|
|
self.browser.set_window_size(900, 500)
|
|
|
|
|
|
time.sleep(0.5) # allow Firefox to flush the resize before navigating
|
|
|
|
|
|
self.wait_for(lambda: self.assertTrue(
|
|
|
|
|
|
self.browser.execute_script(
|
|
|
|
|
|
'return window.innerWidth > window.innerHeight'
|
|
|
|
|
|
)
|
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
|
|
def _simulate_drag(self, btn, offset_x):
|
|
|
|
|
|
"""Dispatch JS pointer events directly — more reliable than GeckoDriver drag."""
|
|
|
|
|
|
start_x = btn.rect['x'] + btn.rect['width'] / 2
|
|
|
|
|
|
end_x = start_x + offset_x
|
|
|
|
|
|
self.browser.execute_script("""
|
|
|
|
|
|
var btn = arguments[0], startX = arguments[1], endX = arguments[2];
|
|
|
|
|
|
btn.dispatchEvent(new PointerEvent("pointerdown", {clientX: startX, bubbles: true}));
|
|
|
|
|
|
document.dispatchEvent(new PointerEvent("pointermove", {clientX: endX, bubbles: true}));
|
|
|
|
|
|
document.dispatchEvent(new PointerEvent("pointerup", {clientX: endX, bubbles: true}));
|
|
|
|
|
|
""", btn, start_x, end_x)
|
|
|
|
|
|
|
|
|
|
|
|
def _simulate_drag_y(self, btn, offset_y):
|
|
|
|
|
|
"""Dispatch JS pointer events on the Y axis for landscape drag tests."""
|
|
|
|
|
|
start_y = btn.rect['y'] + btn.rect['height'] / 2
|
|
|
|
|
|
end_y = start_y + offset_y
|
|
|
|
|
|
self.browser.execute_script("""
|
|
|
|
|
|
var btn = arguments[0], startY = arguments[1], endY = arguments[2];
|
|
|
|
|
|
btn.dispatchEvent(new PointerEvent("pointerdown", {clientY: startY, clientX: 0, bubbles: true}));
|
|
|
|
|
|
document.dispatchEvent(new PointerEvent("pointermove", {clientY: endY, clientX: 0, bubbles: true}));
|
|
|
|
|
|
document.dispatchEvent(new PointerEvent("pointerup", {clientY: endY, clientX: 0, bubbles: true}));
|
|
|
|
|
|
""", btn, start_y, end_y)
|
|
|
|
|
|
|
|
|
|
|
|
def _make_role_select_room(self, founder_email="founder@test.io"):
|
|
|
|
|
|
from apps.epic.models import TableSeat
|
|
|
|
|
|
founder, _ = User.objects.get_or_create(email=founder_email)
|
|
|
|
|
|
room = Room.objects.create(name="Tray Test Room", owner=founder)
|
|
|
|
|
|
emails = [founder_email, "nc@test.io", "bud@test.io",
|
|
|
|
|
|
"pal@test.io", "dude@test.io", "bro@test.io"]
|
|
|
|
|
|
_fill_room_via_orm(room, emails)
|
|
|
|
|
|
room.table_status = Room.ROLE_SELECT
|
|
|
|
|
|
room.save()
|
|
|
|
|
|
for i, email in enumerate(emails, start=1):
|
|
|
|
|
|
gamer, _ = User.objects.get_or_create(email=email)
|
|
|
|
|
|
TableSeat.objects.get_or_create(room=room, gamer=gamer, slot_number=i)
|
|
|
|
|
|
return room
|
|
|
|
|
|
|
|
|
|
|
|
def _make_sig_select_room(self, founder_email="founder@test.io"):
|
|
|
|
|
|
founder, _ = User.objects.get_or_create(email=founder_email)
|
|
|
|
|
|
room = Room.objects.create(name="Tray Test Room", owner=founder)
|
|
|
|
|
|
_fill_room_via_orm(room, [
|
|
|
|
|
|
founder_email, "nc@test.io", "bud@test.io",
|
|
|
|
|
|
"pal@test.io", "dude@test.io", "bro@test.io",
|
|
|
|
|
|
])
|
|
|
|
|
|
_assign_all_roles(room)
|
|
|
|
|
|
return room
|
|
|
|
|
|
|
|
|
|
|
|
def _room_url(self, room):
|
|
|
|
|
|
return f"{self.live_server_url}/gameboard/room/{room.id}/gate/"
|
|
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
# Test T1 — tray button is present and anchored to the right edge #
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
|
|
|
|
|
|
def test_tray_btn_is_present_on_room_page(self):
|
|
|
|
|
|
room = self._make_sig_select_room()
|
|
|
|
|
|
self.create_pre_authenticated_session("founder@test.io")
|
|
|
|
|
|
self.browser.get(self._room_url(room))
|
|
|
|
|
|
|
|
|
|
|
|
btn = self.wait_for(
|
|
|
|
|
|
lambda: self.browser.find_element(By.ID, "id_tray_btn")
|
2026-03-24 21:07:01 -04:00
|
|
|
|
)
|
functional_tests + CI: rename pass + structural consolidations + parallel test-FTs split — every FT file now starts with one of 6 prefixes (test_admin_* / test_bill_* / test_core_* / test_dash_* / test_game_room_* / test_trinket_*) plus the 4 page-roots test_billboard / test_dashboard / test_gameboard / test_jasmine, so the partition is unambiguous and stable for tooling (the previous mix of test_applet_*, test_room_*, test_component_*, ad-hoc names had no consistent grouping); session-side: merged test_gatekeeper_bud_btn.py into test_bud_btn.py (then user renamed to test_core_bud_btn.py) — both files drove the same #id_bud_btn UI in two contexts (post-share + gatekeeper invite) and shared the bud-btn.js skeleton, so consolidation was overdue; split test_component_cards_tarot.py into test_admin_tarot.py (just TarotAdminTest, sitting next to test_admin / test_admin_post_readonly) + 3 classes (TarotDeckTest / GameKitDeckSelectionTest / GameKitPageTest) appended to test_game_room_tray.py; updated stale `test_bud_btn.py` references in the test_core_bud_btn.py docstring + test_admin_post_readonly.py comment to point at the new filename; user-driven renames (22 files): test_applet_my_notes/posts → test_bill_my_*, test_applet_new_post[_line_validation] → test_bill_new_post[_line_validation], test_applet_my_sky → test_dash_my_sky, test_applet_palette → test_dash_palette, test_wallet → test_dash_wallet, test_login → test_core_login, test_navbar → test_core_navbar, test_sharing → test_core_sharing, test_layout_and_styling → test_core_styling, test_my_buds → test_bill_my_buds, test_bud_btn → test_core_bud_btn, test_deck_contribution → test_game_room_deck_contrib, test_game_invite → test_game_room_invite, test_room_gatekeeper → test_game_room_gatekeeper, test_room_role_select → test_game_room_select_role, test_room_sea_select → test_game_room_select_sea, test_room_sig_select → test_game_room_select_sig, test_room_sky_select → test_game_room_select_sky, test_room_tray → test_game_room_tray, test_component_tray_tooltip → test_game_room_tray_tooltip; the post_page.py / room_page.py helper modules from the May-12 sprint absorbed the cross-file FT imports that would otherwise have cascade-broken on these renames
.woodpecker/main.yaml — CI test-FTs step splits into parallel siblings test-FTs-non-room (22 files via `ls functional_tests/test_*.py | grep -v 'test_game_room_'`) + test-FTs-room (9 files via `ls functional_tests/test_game_room_*.py`); room cluster is the heaviest (~70% of the pre-split ~40-min wall-clock) and now runs concurrently w. the rest instead of in series; DAG explicit via depends_on on every step (Woodpecker mixes default-sequential w. depends_on awkwardly, so each step pins its prerequisite); collectstatic stays in test-two-browser-FTs only — the shared workspace propagates assets to both parallel FT steps, no race + no duplication; screendumps + build-and-push fan back in (depends_on both parallel steps); deploy-staging + deploy-prod depend on build-and-push
smoke-import: 31/31 FT modules green after the rename pass
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 20:06:25 -04:00
|
|
|
|
self.assertTrue(btn.is_displayed())
|
|
|
|
|
|
|
|
|
|
|
|
# Button should be anchored near the right edge of the viewport
|
|
|
|
|
|
vp_width = self.browser.execute_script("return window.innerWidth")
|
|
|
|
|
|
btn_right = btn.location["x"] + btn.size["width"]
|
|
|
|
|
|
self.assertGreater(btn_right, vp_width * 0.8)
|
|
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
# Test T2 — tray is closed by default; clicking wobbles the handle #
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
|
|
|
|
|
|
def test_tray_is_closed_by_default_and_click_wobbles(self):
|
|
|
|
|
|
room = self._make_sig_select_room()
|
|
|
|
|
|
self.create_pre_authenticated_session("founder@test.io")
|
|
|
|
|
|
self.browser.get(self._room_url(room))
|
|
|
|
|
|
|
|
|
|
|
|
self.wait_for(lambda: self.browser.find_element(By.ID, "id_tray_btn"))
|
|
|
|
|
|
|
|
|
|
|
|
# Tray panel not visible when closed
|
|
|
|
|
|
tray = self.browser.find_element(By.ID, "id_tray")
|
|
|
|
|
|
self.assertFalse(tray.is_displayed())
|
|
|
|
|
|
|
|
|
|
|
|
# Clicking the closed btn adds a wobble class to the wrap.
|
|
|
|
|
|
# Use a MutationObserver to capture the transient class change — in CI
|
|
|
|
|
|
# headless Firefox the 0.45s animation may complete before the first
|
|
|
|
|
|
# wait_for poll (0.5s), causing a false miss.
|
|
|
|
|
|
self.browser.execute_script("""
|
|
|
|
|
|
window._trayWobbled = false;
|
|
|
|
|
|
var wrap = document.getElementById('id_tray_wrap');
|
|
|
|
|
|
var obs = new MutationObserver(function(muts) {
|
|
|
|
|
|
muts.forEach(function(m) {
|
|
|
|
|
|
if (m.type === 'attributes' && m.attributeName === 'class') {
|
|
|
|
|
|
if (m.target.classList.contains('wobble')) {
|
|
|
|
|
|
window._trayWobbled = true;
|
|
|
|
|
|
obs.disconnect();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
obs.observe(wrap, {attributes: true, attributeFilter: ['class']});
|
|
|
|
|
|
""")
|
|
|
|
|
|
self.browser.find_element(By.ID, "id_tray_btn").click()
|
|
|
|
|
|
self.wait_for(
|
|
|
|
|
|
lambda: self.assertTrue(
|
|
|
|
|
|
self.browser.execute_script("return window._trayWobbled;")
|
2026-03-24 21:07:01 -04:00
|
|
|
|
)
|
|
|
|
|
|
)
|
functional_tests + CI: rename pass + structural consolidations + parallel test-FTs split — every FT file now starts with one of 6 prefixes (test_admin_* / test_bill_* / test_core_* / test_dash_* / test_game_room_* / test_trinket_*) plus the 4 page-roots test_billboard / test_dashboard / test_gameboard / test_jasmine, so the partition is unambiguous and stable for tooling (the previous mix of test_applet_*, test_room_*, test_component_*, ad-hoc names had no consistent grouping); session-side: merged test_gatekeeper_bud_btn.py into test_bud_btn.py (then user renamed to test_core_bud_btn.py) — both files drove the same #id_bud_btn UI in two contexts (post-share + gatekeeper invite) and shared the bud-btn.js skeleton, so consolidation was overdue; split test_component_cards_tarot.py into test_admin_tarot.py (just TarotAdminTest, sitting next to test_admin / test_admin_post_readonly) + 3 classes (TarotDeckTest / GameKitDeckSelectionTest / GameKitPageTest) appended to test_game_room_tray.py; updated stale `test_bud_btn.py` references in the test_core_bud_btn.py docstring + test_admin_post_readonly.py comment to point at the new filename; user-driven renames (22 files): test_applet_my_notes/posts → test_bill_my_*, test_applet_new_post[_line_validation] → test_bill_new_post[_line_validation], test_applet_my_sky → test_dash_my_sky, test_applet_palette → test_dash_palette, test_wallet → test_dash_wallet, test_login → test_core_login, test_navbar → test_core_navbar, test_sharing → test_core_sharing, test_layout_and_styling → test_core_styling, test_my_buds → test_bill_my_buds, test_bud_btn → test_core_bud_btn, test_deck_contribution → test_game_room_deck_contrib, test_game_invite → test_game_room_invite, test_room_gatekeeper → test_game_room_gatekeeper, test_room_role_select → test_game_room_select_role, test_room_sea_select → test_game_room_select_sea, test_room_sig_select → test_game_room_select_sig, test_room_sky_select → test_game_room_select_sky, test_room_tray → test_game_room_tray, test_component_tray_tooltip → test_game_room_tray_tooltip; the post_page.py / room_page.py helper modules from the May-12 sprint absorbed the cross-file FT imports that would otherwise have cascade-broken on these renames
.woodpecker/main.yaml — CI test-FTs step splits into parallel siblings test-FTs-non-room (22 files via `ls functional_tests/test_*.py | grep -v 'test_game_room_'`) + test-FTs-room (9 files via `ls functional_tests/test_game_room_*.py`); room cluster is the heaviest (~70% of the pre-split ~40-min wall-clock) and now runs concurrently w. the rest instead of in series; DAG explicit via depends_on on every step (Woodpecker mixes default-sequential w. depends_on awkwardly, so each step pins its prerequisite); collectstatic stays in test-two-browser-FTs only — the shared workspace propagates assets to both parallel FT steps, no race + no duplication; screendumps + build-and-push fan back in (depends_on both parallel steps); deploy-staging + deploy-prod depend on build-and-push
smoke-import: 31/31 FT modules green after the rename pass
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 20:06:25 -04:00
|
|
|
|
# Tray still not visible — a click alone must not open it
|
|
|
|
|
|
self.assertFalse(tray.is_displayed())
|
|
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
# Test T3 — dragging tray btn leftward opens the tray #
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
2026-03-24 21:07:01 -04:00
|
|
|
|
|
functional_tests + CI: rename pass + structural consolidations + parallel test-FTs split — every FT file now starts with one of 6 prefixes (test_admin_* / test_bill_* / test_core_* / test_dash_* / test_game_room_* / test_trinket_*) plus the 4 page-roots test_billboard / test_dashboard / test_gameboard / test_jasmine, so the partition is unambiguous and stable for tooling (the previous mix of test_applet_*, test_room_*, test_component_*, ad-hoc names had no consistent grouping); session-side: merged test_gatekeeper_bud_btn.py into test_bud_btn.py (then user renamed to test_core_bud_btn.py) — both files drove the same #id_bud_btn UI in two contexts (post-share + gatekeeper invite) and shared the bud-btn.js skeleton, so consolidation was overdue; split test_component_cards_tarot.py into test_admin_tarot.py (just TarotAdminTest, sitting next to test_admin / test_admin_post_readonly) + 3 classes (TarotDeckTest / GameKitDeckSelectionTest / GameKitPageTest) appended to test_game_room_tray.py; updated stale `test_bud_btn.py` references in the test_core_bud_btn.py docstring + test_admin_post_readonly.py comment to point at the new filename; user-driven renames (22 files): test_applet_my_notes/posts → test_bill_my_*, test_applet_new_post[_line_validation] → test_bill_new_post[_line_validation], test_applet_my_sky → test_dash_my_sky, test_applet_palette → test_dash_palette, test_wallet → test_dash_wallet, test_login → test_core_login, test_navbar → test_core_navbar, test_sharing → test_core_sharing, test_layout_and_styling → test_core_styling, test_my_buds → test_bill_my_buds, test_bud_btn → test_core_bud_btn, test_deck_contribution → test_game_room_deck_contrib, test_game_invite → test_game_room_invite, test_room_gatekeeper → test_game_room_gatekeeper, test_room_role_select → test_game_room_select_role, test_room_sea_select → test_game_room_select_sea, test_room_sig_select → test_game_room_select_sig, test_room_sky_select → test_game_room_select_sky, test_room_tray → test_game_room_tray, test_component_tray_tooltip → test_game_room_tray_tooltip; the post_page.py / room_page.py helper modules from the May-12 sprint absorbed the cross-file FT imports that would otherwise have cascade-broken on these renames
.woodpecker/main.yaml — CI test-FTs step splits into parallel siblings test-FTs-non-room (22 files via `ls functional_tests/test_*.py | grep -v 'test_game_room_'`) + test-FTs-room (9 files via `ls functional_tests/test_game_room_*.py`); room cluster is the heaviest (~70% of the pre-split ~40-min wall-clock) and now runs concurrently w. the rest instead of in series; DAG explicit via depends_on on every step (Woodpecker mixes default-sequential w. depends_on awkwardly, so each step pins its prerequisite); collectstatic stays in test-two-browser-FTs only — the shared workspace propagates assets to both parallel FT steps, no race + no duplication; screendumps + build-and-push fan back in (depends_on both parallel steps); deploy-staging + deploy-prod depend on build-and-push
smoke-import: 31/31 FT modules green after the rename pass
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 20:06:25 -04:00
|
|
|
|
def test_dragging_tray_btn_left_opens_tray(self):
|
|
|
|
|
|
room = self._make_sig_select_room()
|
|
|
|
|
|
self.create_pre_authenticated_session("founder@test.io")
|
|
|
|
|
|
self.browser.get(self._room_url(room))
|
|
|
|
|
|
|
|
|
|
|
|
btn = self.wait_for(lambda: self.browser.find_element(By.ID, "id_tray_btn"))
|
|
|
|
|
|
tray = self.browser.find_element(By.ID, "id_tray")
|
|
|
|
|
|
self.assertFalse(tray.is_displayed())
|
|
|
|
|
|
|
|
|
|
|
|
self._simulate_drag(btn, -300)
|
|
|
|
|
|
|
|
|
|
|
|
self.wait_for(lambda: self.assertTrue(tray.is_displayed()))
|
2026-03-24 21:07:01 -04:00
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
functional_tests + CI: rename pass + structural consolidations + parallel test-FTs split — every FT file now starts with one of 6 prefixes (test_admin_* / test_bill_* / test_core_* / test_dash_* / test_game_room_* / test_trinket_*) plus the 4 page-roots test_billboard / test_dashboard / test_gameboard / test_jasmine, so the partition is unambiguous and stable for tooling (the previous mix of test_applet_*, test_room_*, test_component_*, ad-hoc names had no consistent grouping); session-side: merged test_gatekeeper_bud_btn.py into test_bud_btn.py (then user renamed to test_core_bud_btn.py) — both files drove the same #id_bud_btn UI in two contexts (post-share + gatekeeper invite) and shared the bud-btn.js skeleton, so consolidation was overdue; split test_component_cards_tarot.py into test_admin_tarot.py (just TarotAdminTest, sitting next to test_admin / test_admin_post_readonly) + 3 classes (TarotDeckTest / GameKitDeckSelectionTest / GameKitPageTest) appended to test_game_room_tray.py; updated stale `test_bud_btn.py` references in the test_core_bud_btn.py docstring + test_admin_post_readonly.py comment to point at the new filename; user-driven renames (22 files): test_applet_my_notes/posts → test_bill_my_*, test_applet_new_post[_line_validation] → test_bill_new_post[_line_validation], test_applet_my_sky → test_dash_my_sky, test_applet_palette → test_dash_palette, test_wallet → test_dash_wallet, test_login → test_core_login, test_navbar → test_core_navbar, test_sharing → test_core_sharing, test_layout_and_styling → test_core_styling, test_my_buds → test_bill_my_buds, test_bud_btn → test_core_bud_btn, test_deck_contribution → test_game_room_deck_contrib, test_game_invite → test_game_room_invite, test_room_gatekeeper → test_game_room_gatekeeper, test_room_role_select → test_game_room_select_role, test_room_sea_select → test_game_room_select_sea, test_room_sig_select → test_game_room_select_sig, test_room_sky_select → test_game_room_select_sky, test_room_tray → test_game_room_tray, test_component_tray_tooltip → test_game_room_tray_tooltip; the post_page.py / room_page.py helper modules from the May-12 sprint absorbed the cross-file FT imports that would otherwise have cascade-broken on these renames
.woodpecker/main.yaml — CI test-FTs step splits into parallel siblings test-FTs-non-room (22 files via `ls functional_tests/test_*.py | grep -v 'test_game_room_'`) + test-FTs-room (9 files via `ls functional_tests/test_game_room_*.py`); room cluster is the heaviest (~70% of the pre-split ~40-min wall-clock) and now runs concurrently w. the rest instead of in series; DAG explicit via depends_on on every step (Woodpecker mixes default-sequential w. depends_on awkwardly, so each step pins its prerequisite); collectstatic stays in test-two-browser-FTs only — the shared workspace propagates assets to both parallel FT steps, no race + no duplication; screendumps + build-and-push fan back in (depends_on both parallel steps); deploy-staging + deploy-prod depend on build-and-push
smoke-import: 31/31 FT modules green after the rename pass
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 20:06:25 -04:00
|
|
|
|
# Test T4 — clicking btn while tray is open slides it closed #
|
2026-03-24 21:07:01 -04:00
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
|
functional_tests + CI: rename pass + structural consolidations + parallel test-FTs split — every FT file now starts with one of 6 prefixes (test_admin_* / test_bill_* / test_core_* / test_dash_* / test_game_room_* / test_trinket_*) plus the 4 page-roots test_billboard / test_dashboard / test_gameboard / test_jasmine, so the partition is unambiguous and stable for tooling (the previous mix of test_applet_*, test_room_*, test_component_*, ad-hoc names had no consistent grouping); session-side: merged test_gatekeeper_bud_btn.py into test_bud_btn.py (then user renamed to test_core_bud_btn.py) — both files drove the same #id_bud_btn UI in two contexts (post-share + gatekeeper invite) and shared the bud-btn.js skeleton, so consolidation was overdue; split test_component_cards_tarot.py into test_admin_tarot.py (just TarotAdminTest, sitting next to test_admin / test_admin_post_readonly) + 3 classes (TarotDeckTest / GameKitDeckSelectionTest / GameKitPageTest) appended to test_game_room_tray.py; updated stale `test_bud_btn.py` references in the test_core_bud_btn.py docstring + test_admin_post_readonly.py comment to point at the new filename; user-driven renames (22 files): test_applet_my_notes/posts → test_bill_my_*, test_applet_new_post[_line_validation] → test_bill_new_post[_line_validation], test_applet_my_sky → test_dash_my_sky, test_applet_palette → test_dash_palette, test_wallet → test_dash_wallet, test_login → test_core_login, test_navbar → test_core_navbar, test_sharing → test_core_sharing, test_layout_and_styling → test_core_styling, test_my_buds → test_bill_my_buds, test_bud_btn → test_core_bud_btn, test_deck_contribution → test_game_room_deck_contrib, test_game_invite → test_game_room_invite, test_room_gatekeeper → test_game_room_gatekeeper, test_room_role_select → test_game_room_select_role, test_room_sea_select → test_game_room_select_sea, test_room_sig_select → test_game_room_select_sig, test_room_sky_select → test_game_room_select_sky, test_room_tray → test_game_room_tray, test_component_tray_tooltip → test_game_room_tray_tooltip; the post_page.py / room_page.py helper modules from the May-12 sprint absorbed the cross-file FT imports that would otherwise have cascade-broken on these renames
.woodpecker/main.yaml — CI test-FTs step splits into parallel siblings test-FTs-non-room (22 files via `ls functional_tests/test_*.py | grep -v 'test_game_room_'`) + test-FTs-room (9 files via `ls functional_tests/test_game_room_*.py`); room cluster is the heaviest (~70% of the pre-split ~40-min wall-clock) and now runs concurrently w. the rest instead of in series; DAG explicit via depends_on on every step (Woodpecker mixes default-sequential w. depends_on awkwardly, so each step pins its prerequisite); collectstatic stays in test-two-browser-FTs only — the shared workspace propagates assets to both parallel FT steps, no race + no duplication; screendumps + build-and-push fan back in (depends_on both parallel steps); deploy-staging + deploy-prod depend on build-and-push
smoke-import: 31/31 FT modules green after the rename pass
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 20:06:25 -04:00
|
|
|
|
def test_clicking_open_tray_btn_closes_tray(self):
|
|
|
|
|
|
room = self._make_sig_select_room()
|
|
|
|
|
|
self.create_pre_authenticated_session("founder@test.io")
|
|
|
|
|
|
self.browser.get(self._room_url(room))
|
|
|
|
|
|
|
|
|
|
|
|
btn = self.wait_for(lambda: self.browser.find_element(By.ID, "id_tray_btn"))
|
|
|
|
|
|
self._simulate_drag(btn, -300)
|
2026-03-24 21:07:01 -04:00
|
|
|
|
|
functional_tests + CI: rename pass + structural consolidations + parallel test-FTs split — every FT file now starts with one of 6 prefixes (test_admin_* / test_bill_* / test_core_* / test_dash_* / test_game_room_* / test_trinket_*) plus the 4 page-roots test_billboard / test_dashboard / test_gameboard / test_jasmine, so the partition is unambiguous and stable for tooling (the previous mix of test_applet_*, test_room_*, test_component_*, ad-hoc names had no consistent grouping); session-side: merged test_gatekeeper_bud_btn.py into test_bud_btn.py (then user renamed to test_core_bud_btn.py) — both files drove the same #id_bud_btn UI in two contexts (post-share + gatekeeper invite) and shared the bud-btn.js skeleton, so consolidation was overdue; split test_component_cards_tarot.py into test_admin_tarot.py (just TarotAdminTest, sitting next to test_admin / test_admin_post_readonly) + 3 classes (TarotDeckTest / GameKitDeckSelectionTest / GameKitPageTest) appended to test_game_room_tray.py; updated stale `test_bud_btn.py` references in the test_core_bud_btn.py docstring + test_admin_post_readonly.py comment to point at the new filename; user-driven renames (22 files): test_applet_my_notes/posts → test_bill_my_*, test_applet_new_post[_line_validation] → test_bill_new_post[_line_validation], test_applet_my_sky → test_dash_my_sky, test_applet_palette → test_dash_palette, test_wallet → test_dash_wallet, test_login → test_core_login, test_navbar → test_core_navbar, test_sharing → test_core_sharing, test_layout_and_styling → test_core_styling, test_my_buds → test_bill_my_buds, test_bud_btn → test_core_bud_btn, test_deck_contribution → test_game_room_deck_contrib, test_game_invite → test_game_room_invite, test_room_gatekeeper → test_game_room_gatekeeper, test_room_role_select → test_game_room_select_role, test_room_sea_select → test_game_room_select_sea, test_room_sig_select → test_game_room_select_sig, test_room_sky_select → test_game_room_select_sky, test_room_tray → test_game_room_tray, test_component_tray_tooltip → test_game_room_tray_tooltip; the post_page.py / room_page.py helper modules from the May-12 sprint absorbed the cross-file FT imports that would otherwise have cascade-broken on these renames
.woodpecker/main.yaml — CI test-FTs step splits into parallel siblings test-FTs-non-room (22 files via `ls functional_tests/test_*.py | grep -v 'test_game_room_'`) + test-FTs-room (9 files via `ls functional_tests/test_game_room_*.py`); room cluster is the heaviest (~70% of the pre-split ~40-min wall-clock) and now runs concurrently w. the rest instead of in series; DAG explicit via depends_on on every step (Woodpecker mixes default-sequential w. depends_on awkwardly, so each step pins its prerequisite); collectstatic stays in test-two-browser-FTs only — the shared workspace propagates assets to both parallel FT steps, no race + no duplication; screendumps + build-and-push fan back in (depends_on both parallel steps); deploy-staging + deploy-prod depend on build-and-push
smoke-import: 31/31 FT modules green after the rename pass
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 20:06:25 -04:00
|
|
|
|
tray = self.browser.find_element(By.ID, "id_tray")
|
|
|
|
|
|
self.wait_for(lambda: self.assertTrue(tray.is_displayed()))
|
|
|
|
|
|
|
|
|
|
|
|
self.browser.find_element(By.ID, "id_tray_btn").click()
|
|
|
|
|
|
self.wait_for(lambda: self.assertFalse(tray.is_displayed()))
|
2026-03-24 21:07:01 -04:00
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
functional_tests + CI: rename pass + structural consolidations + parallel test-FTs split — every FT file now starts with one of 6 prefixes (test_admin_* / test_bill_* / test_core_* / test_dash_* / test_game_room_* / test_trinket_*) plus the 4 page-roots test_billboard / test_dashboard / test_gameboard / test_jasmine, so the partition is unambiguous and stable for tooling (the previous mix of test_applet_*, test_room_*, test_component_*, ad-hoc names had no consistent grouping); session-side: merged test_gatekeeper_bud_btn.py into test_bud_btn.py (then user renamed to test_core_bud_btn.py) — both files drove the same #id_bud_btn UI in two contexts (post-share + gatekeeper invite) and shared the bud-btn.js skeleton, so consolidation was overdue; split test_component_cards_tarot.py into test_admin_tarot.py (just TarotAdminTest, sitting next to test_admin / test_admin_post_readonly) + 3 classes (TarotDeckTest / GameKitDeckSelectionTest / GameKitPageTest) appended to test_game_room_tray.py; updated stale `test_bud_btn.py` references in the test_core_bud_btn.py docstring + test_admin_post_readonly.py comment to point at the new filename; user-driven renames (22 files): test_applet_my_notes/posts → test_bill_my_*, test_applet_new_post[_line_validation] → test_bill_new_post[_line_validation], test_applet_my_sky → test_dash_my_sky, test_applet_palette → test_dash_palette, test_wallet → test_dash_wallet, test_login → test_core_login, test_navbar → test_core_navbar, test_sharing → test_core_sharing, test_layout_and_styling → test_core_styling, test_my_buds → test_bill_my_buds, test_bud_btn → test_core_bud_btn, test_deck_contribution → test_game_room_deck_contrib, test_game_invite → test_game_room_invite, test_room_gatekeeper → test_game_room_gatekeeper, test_room_role_select → test_game_room_select_role, test_room_sea_select → test_game_room_select_sea, test_room_sig_select → test_game_room_select_sig, test_room_sky_select → test_game_room_select_sky, test_room_tray → test_game_room_tray, test_component_tray_tooltip → test_game_room_tray_tooltip; the post_page.py / room_page.py helper modules from the May-12 sprint absorbed the cross-file FT imports that would otherwise have cascade-broken on these renames
.woodpecker/main.yaml — CI test-FTs step splits into parallel siblings test-FTs-non-room (22 files via `ls functional_tests/test_*.py | grep -v 'test_game_room_'`) + test-FTs-room (9 files via `ls functional_tests/test_game_room_*.py`); room cluster is the heaviest (~70% of the pre-split ~40-min wall-clock) and now runs concurrently w. the rest instead of in series; DAG explicit via depends_on on every step (Woodpecker mixes default-sequential w. depends_on awkwardly, so each step pins its prerequisite); collectstatic stays in test-two-browser-FTs only — the shared workspace propagates assets to both parallel FT steps, no race + no duplication; screendumps + build-and-push fan back in (depends_on both parallel steps); deploy-staging + deploy-prod depend on build-and-push
smoke-import: 31/31 FT modules green after the rename pass
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 20:06:25 -04:00
|
|
|
|
# Test T5 — tray reverts to closed on page reload #
|
2026-03-24 21:07:01 -04:00
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
|
functional_tests + CI: rename pass + structural consolidations + parallel test-FTs split — every FT file now starts with one of 6 prefixes (test_admin_* / test_bill_* / test_core_* / test_dash_* / test_game_room_* / test_trinket_*) plus the 4 page-roots test_billboard / test_dashboard / test_gameboard / test_jasmine, so the partition is unambiguous and stable for tooling (the previous mix of test_applet_*, test_room_*, test_component_*, ad-hoc names had no consistent grouping); session-side: merged test_gatekeeper_bud_btn.py into test_bud_btn.py (then user renamed to test_core_bud_btn.py) — both files drove the same #id_bud_btn UI in two contexts (post-share + gatekeeper invite) and shared the bud-btn.js skeleton, so consolidation was overdue; split test_component_cards_tarot.py into test_admin_tarot.py (just TarotAdminTest, sitting next to test_admin / test_admin_post_readonly) + 3 classes (TarotDeckTest / GameKitDeckSelectionTest / GameKitPageTest) appended to test_game_room_tray.py; updated stale `test_bud_btn.py` references in the test_core_bud_btn.py docstring + test_admin_post_readonly.py comment to point at the new filename; user-driven renames (22 files): test_applet_my_notes/posts → test_bill_my_*, test_applet_new_post[_line_validation] → test_bill_new_post[_line_validation], test_applet_my_sky → test_dash_my_sky, test_applet_palette → test_dash_palette, test_wallet → test_dash_wallet, test_login → test_core_login, test_navbar → test_core_navbar, test_sharing → test_core_sharing, test_layout_and_styling → test_core_styling, test_my_buds → test_bill_my_buds, test_bud_btn → test_core_bud_btn, test_deck_contribution → test_game_room_deck_contrib, test_game_invite → test_game_room_invite, test_room_gatekeeper → test_game_room_gatekeeper, test_room_role_select → test_game_room_select_role, test_room_sea_select → test_game_room_select_sea, test_room_sig_select → test_game_room_select_sig, test_room_sky_select → test_game_room_select_sky, test_room_tray → test_game_room_tray, test_component_tray_tooltip → test_game_room_tray_tooltip; the post_page.py / room_page.py helper modules from the May-12 sprint absorbed the cross-file FT imports that would otherwise have cascade-broken on these renames
.woodpecker/main.yaml — CI test-FTs step splits into parallel siblings test-FTs-non-room (22 files via `ls functional_tests/test_*.py | grep -v 'test_game_room_'`) + test-FTs-room (9 files via `ls functional_tests/test_game_room_*.py`); room cluster is the heaviest (~70% of the pre-split ~40-min wall-clock) and now runs concurrently w. the rest instead of in series; DAG explicit via depends_on on every step (Woodpecker mixes default-sequential w. depends_on awkwardly, so each step pins its prerequisite); collectstatic stays in test-two-browser-FTs only — the shared workspace propagates assets to both parallel FT steps, no race + no duplication; screendumps + build-and-push fan back in (depends_on both parallel steps); deploy-staging + deploy-prod depend on build-and-push
smoke-import: 31/31 FT modules green after the rename pass
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 20:06:25 -04:00
|
|
|
|
def test_tray_reverts_to_closed_on_reload(self):
|
|
|
|
|
|
room = self._make_sig_select_room()
|
|
|
|
|
|
self.create_pre_authenticated_session("founder@test.io")
|
|
|
|
|
|
room_url = self._room_url(room)
|
|
|
|
|
|
self.browser.get(room_url)
|
|
|
|
|
|
|
|
|
|
|
|
btn = self.wait_for(lambda: self.browser.find_element(By.ID, "id_tray_btn"))
|
|
|
|
|
|
self._simulate_drag(btn, -300)
|
2026-03-24 21:07:01 -04:00
|
|
|
|
|
functional_tests + CI: rename pass + structural consolidations + parallel test-FTs split — every FT file now starts with one of 6 prefixes (test_admin_* / test_bill_* / test_core_* / test_dash_* / test_game_room_* / test_trinket_*) plus the 4 page-roots test_billboard / test_dashboard / test_gameboard / test_jasmine, so the partition is unambiguous and stable for tooling (the previous mix of test_applet_*, test_room_*, test_component_*, ad-hoc names had no consistent grouping); session-side: merged test_gatekeeper_bud_btn.py into test_bud_btn.py (then user renamed to test_core_bud_btn.py) — both files drove the same #id_bud_btn UI in two contexts (post-share + gatekeeper invite) and shared the bud-btn.js skeleton, so consolidation was overdue; split test_component_cards_tarot.py into test_admin_tarot.py (just TarotAdminTest, sitting next to test_admin / test_admin_post_readonly) + 3 classes (TarotDeckTest / GameKitDeckSelectionTest / GameKitPageTest) appended to test_game_room_tray.py; updated stale `test_bud_btn.py` references in the test_core_bud_btn.py docstring + test_admin_post_readonly.py comment to point at the new filename; user-driven renames (22 files): test_applet_my_notes/posts → test_bill_my_*, test_applet_new_post[_line_validation] → test_bill_new_post[_line_validation], test_applet_my_sky → test_dash_my_sky, test_applet_palette → test_dash_palette, test_wallet → test_dash_wallet, test_login → test_core_login, test_navbar → test_core_navbar, test_sharing → test_core_sharing, test_layout_and_styling → test_core_styling, test_my_buds → test_bill_my_buds, test_bud_btn → test_core_bud_btn, test_deck_contribution → test_game_room_deck_contrib, test_game_invite → test_game_room_invite, test_room_gatekeeper → test_game_room_gatekeeper, test_room_role_select → test_game_room_select_role, test_room_sea_select → test_game_room_select_sea, test_room_sig_select → test_game_room_select_sig, test_room_sky_select → test_game_room_select_sky, test_room_tray → test_game_room_tray, test_component_tray_tooltip → test_game_room_tray_tooltip; the post_page.py / room_page.py helper modules from the May-12 sprint absorbed the cross-file FT imports that would otherwise have cascade-broken on these renames
.woodpecker/main.yaml — CI test-FTs step splits into parallel siblings test-FTs-non-room (22 files via `ls functional_tests/test_*.py | grep -v 'test_game_room_'`) + test-FTs-room (9 files via `ls functional_tests/test_game_room_*.py`); room cluster is the heaviest (~70% of the pre-split ~40-min wall-clock) and now runs concurrently w. the rest instead of in series; DAG explicit via depends_on on every step (Woodpecker mixes default-sequential w. depends_on awkwardly, so each step pins its prerequisite); collectstatic stays in test-two-browser-FTs only — the shared workspace propagates assets to both parallel FT steps, no race + no duplication; screendumps + build-and-push fan back in (depends_on both parallel steps); deploy-staging + deploy-prod depend on build-and-push
smoke-import: 31/31 FT modules green after the rename pass
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 20:06:25 -04:00
|
|
|
|
tray = self.browser.find_element(By.ID, "id_tray")
|
|
|
|
|
|
self.wait_for(lambda: self.assertTrue(tray.is_displayed()))
|
|
|
|
|
|
|
|
|
|
|
|
# Reload — tray must start closed regardless of previous state
|
|
|
|
|
|
self.browser.get(room_url)
|
|
|
|
|
|
self.wait_for(lambda: self.browser.find_element(By.ID, "id_tray_btn"))
|
|
|
|
|
|
tray = self.browser.find_element(By.ID, "id_tray")
|
|
|
|
|
|
self.assertFalse(tray.is_displayed())
|
2026-03-24 21:07:01 -04:00
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
functional_tests + CI: rename pass + structural consolidations + parallel test-FTs split — every FT file now starts with one of 6 prefixes (test_admin_* / test_bill_* / test_core_* / test_dash_* / test_game_room_* / test_trinket_*) plus the 4 page-roots test_billboard / test_dashboard / test_gameboard / test_jasmine, so the partition is unambiguous and stable for tooling (the previous mix of test_applet_*, test_room_*, test_component_*, ad-hoc names had no consistent grouping); session-side: merged test_gatekeeper_bud_btn.py into test_bud_btn.py (then user renamed to test_core_bud_btn.py) — both files drove the same #id_bud_btn UI in two contexts (post-share + gatekeeper invite) and shared the bud-btn.js skeleton, so consolidation was overdue; split test_component_cards_tarot.py into test_admin_tarot.py (just TarotAdminTest, sitting next to test_admin / test_admin_post_readonly) + 3 classes (TarotDeckTest / GameKitDeckSelectionTest / GameKitPageTest) appended to test_game_room_tray.py; updated stale `test_bud_btn.py` references in the test_core_bud_btn.py docstring + test_admin_post_readonly.py comment to point at the new filename; user-driven renames (22 files): test_applet_my_notes/posts → test_bill_my_*, test_applet_new_post[_line_validation] → test_bill_new_post[_line_validation], test_applet_my_sky → test_dash_my_sky, test_applet_palette → test_dash_palette, test_wallet → test_dash_wallet, test_login → test_core_login, test_navbar → test_core_navbar, test_sharing → test_core_sharing, test_layout_and_styling → test_core_styling, test_my_buds → test_bill_my_buds, test_bud_btn → test_core_bud_btn, test_deck_contribution → test_game_room_deck_contrib, test_game_invite → test_game_room_invite, test_room_gatekeeper → test_game_room_gatekeeper, test_room_role_select → test_game_room_select_role, test_room_sea_select → test_game_room_select_sea, test_room_sig_select → test_game_room_select_sig, test_room_sky_select → test_game_room_select_sky, test_room_tray → test_game_room_tray, test_component_tray_tooltip → test_game_room_tray_tooltip; the post_page.py / room_page.py helper modules from the May-12 sprint absorbed the cross-file FT imports that would otherwise have cascade-broken on these renames
.woodpecker/main.yaml — CI test-FTs step splits into parallel siblings test-FTs-non-room (22 files via `ls functional_tests/test_*.py | grep -v 'test_game_room_'`) + test-FTs-room (9 files via `ls functional_tests/test_game_room_*.py`); room cluster is the heaviest (~70% of the pre-split ~40-min wall-clock) and now runs concurrently w. the rest instead of in series; DAG explicit via depends_on on every step (Woodpecker mixes default-sequential w. depends_on awkwardly, so each step pins its prerequisite); collectstatic stays in test-two-browser-FTs only — the shared workspace propagates assets to both parallel FT steps, no race + no duplication; screendumps + build-and-push fan back in (depends_on both parallel steps); deploy-staging + deploy-prod depend on build-and-push
smoke-import: 31/31 FT modules green after the rename pass
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 20:06:25 -04:00
|
|
|
|
# Test T6 — landscape: tray btn is near the top edge of the viewport #
|
2026-03-24 21:07:01 -04:00
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
|
functional_tests + CI: rename pass + structural consolidations + parallel test-FTs split — every FT file now starts with one of 6 prefixes (test_admin_* / test_bill_* / test_core_* / test_dash_* / test_game_room_* / test_trinket_*) plus the 4 page-roots test_billboard / test_dashboard / test_gameboard / test_jasmine, so the partition is unambiguous and stable for tooling (the previous mix of test_applet_*, test_room_*, test_component_*, ad-hoc names had no consistent grouping); session-side: merged test_gatekeeper_bud_btn.py into test_bud_btn.py (then user renamed to test_core_bud_btn.py) — both files drove the same #id_bud_btn UI in two contexts (post-share + gatekeeper invite) and shared the bud-btn.js skeleton, so consolidation was overdue; split test_component_cards_tarot.py into test_admin_tarot.py (just TarotAdminTest, sitting next to test_admin / test_admin_post_readonly) + 3 classes (TarotDeckTest / GameKitDeckSelectionTest / GameKitPageTest) appended to test_game_room_tray.py; updated stale `test_bud_btn.py` references in the test_core_bud_btn.py docstring + test_admin_post_readonly.py comment to point at the new filename; user-driven renames (22 files): test_applet_my_notes/posts → test_bill_my_*, test_applet_new_post[_line_validation] → test_bill_new_post[_line_validation], test_applet_my_sky → test_dash_my_sky, test_applet_palette → test_dash_palette, test_wallet → test_dash_wallet, test_login → test_core_login, test_navbar → test_core_navbar, test_sharing → test_core_sharing, test_layout_and_styling → test_core_styling, test_my_buds → test_bill_my_buds, test_bud_btn → test_core_bud_btn, test_deck_contribution → test_game_room_deck_contrib, test_game_invite → test_game_room_invite, test_room_gatekeeper → test_game_room_gatekeeper, test_room_role_select → test_game_room_select_role, test_room_sea_select → test_game_room_select_sea, test_room_sig_select → test_game_room_select_sig, test_room_sky_select → test_game_room_select_sky, test_room_tray → test_game_room_tray, test_component_tray_tooltip → test_game_room_tray_tooltip; the post_page.py / room_page.py helper modules from the May-12 sprint absorbed the cross-file FT imports that would otherwise have cascade-broken on these renames
.woodpecker/main.yaml — CI test-FTs step splits into parallel siblings test-FTs-non-room (22 files via `ls functional_tests/test_*.py | grep -v 'test_game_room_'`) + test-FTs-room (9 files via `ls functional_tests/test_game_room_*.py`); room cluster is the heaviest (~70% of the pre-split ~40-min wall-clock) and now runs concurrently w. the rest instead of in series; DAG explicit via depends_on on every step (Woodpecker mixes default-sequential w. depends_on awkwardly, so each step pins its prerequisite); collectstatic stays in test-two-browser-FTs only — the shared workspace propagates assets to both parallel FT steps, no race + no duplication; screendumps + build-and-push fan back in (depends_on both parallel steps); deploy-staging + deploy-prod depend on build-and-push
smoke-import: 31/31 FT modules green after the rename pass
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 20:06:25 -04:00
|
|
|
|
@tag('two-browser')
|
|
|
|
|
|
def test_tray_btn_anchored_near_top_in_landscape(self):
|
|
|
|
|
|
room = self._make_sig_select_room()
|
|
|
|
|
|
self._switch_to_landscape()
|
|
|
|
|
|
self.create_pre_authenticated_session("founder@test.io")
|
|
|
|
|
|
self.browser.get(self._room_url(room))
|
2026-03-24 21:07:01 -04:00
|
|
|
|
|
functional_tests + CI: rename pass + structural consolidations + parallel test-FTs split — every FT file now starts with one of 6 prefixes (test_admin_* / test_bill_* / test_core_* / test_dash_* / test_game_room_* / test_trinket_*) plus the 4 page-roots test_billboard / test_dashboard / test_gameboard / test_jasmine, so the partition is unambiguous and stable for tooling (the previous mix of test_applet_*, test_room_*, test_component_*, ad-hoc names had no consistent grouping); session-side: merged test_gatekeeper_bud_btn.py into test_bud_btn.py (then user renamed to test_core_bud_btn.py) — both files drove the same #id_bud_btn UI in two contexts (post-share + gatekeeper invite) and shared the bud-btn.js skeleton, so consolidation was overdue; split test_component_cards_tarot.py into test_admin_tarot.py (just TarotAdminTest, sitting next to test_admin / test_admin_post_readonly) + 3 classes (TarotDeckTest / GameKitDeckSelectionTest / GameKitPageTest) appended to test_game_room_tray.py; updated stale `test_bud_btn.py` references in the test_core_bud_btn.py docstring + test_admin_post_readonly.py comment to point at the new filename; user-driven renames (22 files): test_applet_my_notes/posts → test_bill_my_*, test_applet_new_post[_line_validation] → test_bill_new_post[_line_validation], test_applet_my_sky → test_dash_my_sky, test_applet_palette → test_dash_palette, test_wallet → test_dash_wallet, test_login → test_core_login, test_navbar → test_core_navbar, test_sharing → test_core_sharing, test_layout_and_styling → test_core_styling, test_my_buds → test_bill_my_buds, test_bud_btn → test_core_bud_btn, test_deck_contribution → test_game_room_deck_contrib, test_game_invite → test_game_room_invite, test_room_gatekeeper → test_game_room_gatekeeper, test_room_role_select → test_game_room_select_role, test_room_sea_select → test_game_room_select_sea, test_room_sig_select → test_game_room_select_sig, test_room_sky_select → test_game_room_select_sky, test_room_tray → test_game_room_tray, test_component_tray_tooltip → test_game_room_tray_tooltip; the post_page.py / room_page.py helper modules from the May-12 sprint absorbed the cross-file FT imports that would otherwise have cascade-broken on these renames
.woodpecker/main.yaml — CI test-FTs step splits into parallel siblings test-FTs-non-room (22 files via `ls functional_tests/test_*.py | grep -v 'test_game_room_'`) + test-FTs-room (9 files via `ls functional_tests/test_game_room_*.py`); room cluster is the heaviest (~70% of the pre-split ~40-min wall-clock) and now runs concurrently w. the rest instead of in series; DAG explicit via depends_on on every step (Woodpecker mixes default-sequential w. depends_on awkwardly, so each step pins its prerequisite); collectstatic stays in test-two-browser-FTs only — the shared workspace propagates assets to both parallel FT steps, no race + no duplication; screendumps + build-and-push fan back in (depends_on both parallel steps); deploy-staging + deploy-prod depend on build-and-push
smoke-import: 31/31 FT modules green after the rename pass
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 20:06:25 -04:00
|
|
|
|
btn = self.wait_for(
|
|
|
|
|
|
lambda: self.browser.find_element(By.ID, "id_tray_btn")
|
|
|
|
|
|
)
|
|
|
|
|
|
self.assertTrue(btn.is_displayed())
|
2026-03-24 21:07:01 -04:00
|
|
|
|
|
functional_tests + CI: rename pass + structural consolidations + parallel test-FTs split — every FT file now starts with one of 6 prefixes (test_admin_* / test_bill_* / test_core_* / test_dash_* / test_game_room_* / test_trinket_*) plus the 4 page-roots test_billboard / test_dashboard / test_gameboard / test_jasmine, so the partition is unambiguous and stable for tooling (the previous mix of test_applet_*, test_room_*, test_component_*, ad-hoc names had no consistent grouping); session-side: merged test_gatekeeper_bud_btn.py into test_bud_btn.py (then user renamed to test_core_bud_btn.py) — both files drove the same #id_bud_btn UI in two contexts (post-share + gatekeeper invite) and shared the bud-btn.js skeleton, so consolidation was overdue; split test_component_cards_tarot.py into test_admin_tarot.py (just TarotAdminTest, sitting next to test_admin / test_admin_post_readonly) + 3 classes (TarotDeckTest / GameKitDeckSelectionTest / GameKitPageTest) appended to test_game_room_tray.py; updated stale `test_bud_btn.py` references in the test_core_bud_btn.py docstring + test_admin_post_readonly.py comment to point at the new filename; user-driven renames (22 files): test_applet_my_notes/posts → test_bill_my_*, test_applet_new_post[_line_validation] → test_bill_new_post[_line_validation], test_applet_my_sky → test_dash_my_sky, test_applet_palette → test_dash_palette, test_wallet → test_dash_wallet, test_login → test_core_login, test_navbar → test_core_navbar, test_sharing → test_core_sharing, test_layout_and_styling → test_core_styling, test_my_buds → test_bill_my_buds, test_bud_btn → test_core_bud_btn, test_deck_contribution → test_game_room_deck_contrib, test_game_invite → test_game_room_invite, test_room_gatekeeper → test_game_room_gatekeeper, test_room_role_select → test_game_room_select_role, test_room_sea_select → test_game_room_select_sea, test_room_sig_select → test_game_room_select_sig, test_room_sky_select → test_game_room_select_sky, test_room_tray → test_game_room_tray, test_component_tray_tooltip → test_game_room_tray_tooltip; the post_page.py / room_page.py helper modules from the May-12 sprint absorbed the cross-file FT imports that would otherwise have cascade-broken on these renames
.woodpecker/main.yaml — CI test-FTs step splits into parallel siblings test-FTs-non-room (22 files via `ls functional_tests/test_*.py | grep -v 'test_game_room_'`) + test-FTs-room (9 files via `ls functional_tests/test_game_room_*.py`); room cluster is the heaviest (~70% of the pre-split ~40-min wall-clock) and now runs concurrently w. the rest instead of in series; DAG explicit via depends_on on every step (Woodpecker mixes default-sequential w. depends_on awkwardly, so each step pins its prerequisite); collectstatic stays in test-two-browser-FTs only — the shared workspace propagates assets to both parallel FT steps, no race + no duplication; screendumps + build-and-push fan back in (depends_on both parallel steps); deploy-staging + deploy-prod depend on build-and-push
smoke-import: 31/31 FT modules green after the rename pass
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 20:06:25 -04:00
|
|
|
|
# In landscape the handle sits at the top of the content area;
|
|
|
|
|
|
# btn bottom should be within the top 40% of the viewport.
|
|
|
|
|
|
vh = self.browser.execute_script("return window.innerHeight")
|
|
|
|
|
|
btn_bottom = btn.location["y"] + btn.size["height"]
|
|
|
|
|
|
self.assertLess(btn_bottom, vh * 0.4)
|
|
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
# Test T7 — landscape: dragging btn downward opens the tray #
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
|
|
|
|
|
|
@tag('two-browser')
|
|
|
|
|
|
def test_dragging_tray_btn_down_opens_tray_in_landscape(self):
|
|
|
|
|
|
room = self._make_sig_select_room()
|
|
|
|
|
|
self._switch_to_landscape()
|
|
|
|
|
|
self.create_pre_authenticated_session("founder@test.io")
|
|
|
|
|
|
self.browser.get(self._room_url(room))
|
2026-03-24 21:07:01 -04:00
|
|
|
|
|
functional_tests + CI: rename pass + structural consolidations + parallel test-FTs split — every FT file now starts with one of 6 prefixes (test_admin_* / test_bill_* / test_core_* / test_dash_* / test_game_room_* / test_trinket_*) plus the 4 page-roots test_billboard / test_dashboard / test_gameboard / test_jasmine, so the partition is unambiguous and stable for tooling (the previous mix of test_applet_*, test_room_*, test_component_*, ad-hoc names had no consistent grouping); session-side: merged test_gatekeeper_bud_btn.py into test_bud_btn.py (then user renamed to test_core_bud_btn.py) — both files drove the same #id_bud_btn UI in two contexts (post-share + gatekeeper invite) and shared the bud-btn.js skeleton, so consolidation was overdue; split test_component_cards_tarot.py into test_admin_tarot.py (just TarotAdminTest, sitting next to test_admin / test_admin_post_readonly) + 3 classes (TarotDeckTest / GameKitDeckSelectionTest / GameKitPageTest) appended to test_game_room_tray.py; updated stale `test_bud_btn.py` references in the test_core_bud_btn.py docstring + test_admin_post_readonly.py comment to point at the new filename; user-driven renames (22 files): test_applet_my_notes/posts → test_bill_my_*, test_applet_new_post[_line_validation] → test_bill_new_post[_line_validation], test_applet_my_sky → test_dash_my_sky, test_applet_palette → test_dash_palette, test_wallet → test_dash_wallet, test_login → test_core_login, test_navbar → test_core_navbar, test_sharing → test_core_sharing, test_layout_and_styling → test_core_styling, test_my_buds → test_bill_my_buds, test_bud_btn → test_core_bud_btn, test_deck_contribution → test_game_room_deck_contrib, test_game_invite → test_game_room_invite, test_room_gatekeeper → test_game_room_gatekeeper, test_room_role_select → test_game_room_select_role, test_room_sea_select → test_game_room_select_sea, test_room_sig_select → test_game_room_select_sig, test_room_sky_select → test_game_room_select_sky, test_room_tray → test_game_room_tray, test_component_tray_tooltip → test_game_room_tray_tooltip; the post_page.py / room_page.py helper modules from the May-12 sprint absorbed the cross-file FT imports that would otherwise have cascade-broken on these renames
.woodpecker/main.yaml — CI test-FTs step splits into parallel siblings test-FTs-non-room (22 files via `ls functional_tests/test_*.py | grep -v 'test_game_room_'`) + test-FTs-room (9 files via `ls functional_tests/test_game_room_*.py`); room cluster is the heaviest (~70% of the pre-split ~40-min wall-clock) and now runs concurrently w. the rest instead of in series; DAG explicit via depends_on on every step (Woodpecker mixes default-sequential w. depends_on awkwardly, so each step pins its prerequisite); collectstatic stays in test-two-browser-FTs only — the shared workspace propagates assets to both parallel FT steps, no race + no duplication; screendumps + build-and-push fan back in (depends_on both parallel steps); deploy-staging + deploy-prod depend on build-and-push
smoke-import: 31/31 FT modules green after the rename pass
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 20:06:25 -04:00
|
|
|
|
btn = self.wait_for(lambda: self.browser.find_element(By.ID, "id_tray_btn"))
|
|
|
|
|
|
# In landscape, #id_tray is always display:block; position controls visibility.
|
|
|
|
|
|
# Use Tray.isOpen() to check logical state.
|
|
|
|
|
|
self.assertFalse(self.browser.execute_script("return Tray.isOpen()"))
|
|
|
|
|
|
|
|
|
|
|
|
self._simulate_drag_y(btn, 300)
|
|
|
|
|
|
|
|
|
|
|
|
self.wait_for(
|
|
|
|
|
|
lambda: self.assertTrue(self.browser.execute_script("return Tray.isOpen()"))
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
# Test T8 — portrait: 1 column × 8 rows of square cells #
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
|
|
|
|
|
|
@unittest.skip("portrait grid layout flaky in CI headless Firefox — revisit")
|
|
|
|
|
|
@tag('two-browser')
|
|
|
|
|
|
def test_tray_grid_is_1_column_by_8_rows_in_portrait(self):
|
|
|
|
|
|
room = self._make_role_select_room()
|
|
|
|
|
|
self.create_pre_authenticated_session("founder@test.io")
|
|
|
|
|
|
self.browser.get(self._room_url(room))
|
|
|
|
|
|
|
|
|
|
|
|
btn = self.wait_for(lambda: self.browser.find_element(By.ID, "id_tray_btn"))
|
|
|
|
|
|
self._simulate_drag(btn, -300)
|
|
|
|
|
|
self.wait_for(
|
|
|
|
|
|
lambda: self.assertTrue(
|
|
|
|
|
|
self.browser.find_element(By.ID, "id_tray").is_displayed()
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
cells = self.browser.find_elements(By.CSS_SELECTOR, "#id_tray_grid .tray-cell")
|
|
|
|
|
|
self.assertEqual(len(cells), 8)
|
|
|
|
|
|
|
|
|
|
|
|
# 8 explicit rows set via grid-template-rows
|
|
|
|
|
|
row_count = self.browser.execute_script("""
|
|
|
|
|
|
var s = getComputedStyle(document.getElementById('id_tray_grid'));
|
|
|
|
|
|
return s.gridTemplateRows.trim().split(/\\s+/).length;
|
|
|
|
|
|
""")
|
|
|
|
|
|
self.assertEqual(row_count, 8)
|
|
|
|
|
|
|
|
|
|
|
|
# All 8 cells share the same x position — one column only
|
|
|
|
|
|
xs = {round(c.location['x']) for c in cells}
|
|
|
|
|
|
self.assertEqual(len(xs), 1)
|
|
|
|
|
|
|
|
|
|
|
|
# Cells are square
|
|
|
|
|
|
cell = cells[0]
|
|
|
|
|
|
self.assertAlmostEqual(cell.size['width'], cell.size['height'], delta=2)
|
|
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
# Test T9 — landscape: 8 columns × 1 row of square cells #
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
# T9a — column/row count (structure)
|
|
|
|
|
|
@unittest.skip("landscape grid layout flaky in CI headless Firefox — revisit")
|
|
|
|
|
|
@tag('two-browser')
|
|
|
|
|
|
def test_tray_grid_is_8_columns_by_1_row_in_landscape(self):
|
|
|
|
|
|
room = self._make_sig_select_room()
|
|
|
|
|
|
self._switch_to_landscape()
|
|
|
|
|
|
self.create_pre_authenticated_session("founder@test.io")
|
|
|
|
|
|
self.browser.get(self._room_url(room))
|
|
|
|
|
|
|
|
|
|
|
|
btn = self.wait_for(lambda: self.browser.find_element(By.ID, "id_tray_btn"))
|
|
|
|
|
|
self._simulate_drag_y(btn, 300)
|
|
|
|
|
|
self.wait_for(
|
|
|
|
|
|
lambda: self.assertTrue(self.browser.execute_script("return Tray.isOpen()"))
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
cells = self.browser.find_elements(By.CSS_SELECTOR, "#id_tray_grid .tray-cell")
|
|
|
|
|
|
self.assertEqual(len(cells), 8)
|
|
|
|
|
|
|
|
|
|
|
|
# 8 explicit columns set via grid-template-columns
|
|
|
|
|
|
col_count = self.browser.execute_script("""
|
|
|
|
|
|
var s = getComputedStyle(document.getElementById('id_tray_grid'));
|
|
|
|
|
|
return s.gridTemplateColumns.trim().split(/\\s+/).length;
|
|
|
|
|
|
""")
|
|
|
|
|
|
self.assertEqual(col_count, 8)
|
|
|
|
|
|
|
|
|
|
|
|
# All 8 cells share the same y position — one row only
|
|
|
|
|
|
ys = {round(c.location['y']) for c in cells}
|
|
|
|
|
|
self.assertEqual(len(ys), 1)
|
|
|
|
|
|
|
|
|
|
|
|
# Cells are square
|
|
|
|
|
|
cell = cells[0]
|
|
|
|
|
|
self.assertAlmostEqual(cell.size['width'], cell.size['height'], delta=2)
|
|
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
# Test T9b — landscape: all 8 cells visible within the tray interior #
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
|
|
|
|
|
|
@unittest.skip("landscape cell bounds flaky in CI headless Firefox — revisit with T9a")
|
|
|
|
|
|
@tag('two-browser')
|
|
|
|
|
|
def test_landscape_tray_all_8_cells_visible(self):
|
|
|
|
|
|
room = self._make_sig_select_room()
|
|
|
|
|
|
self._switch_to_landscape()
|
|
|
|
|
|
self.create_pre_authenticated_session("founder@test.io")
|
|
|
|
|
|
self.browser.get(self._room_url(room))
|
|
|
|
|
|
|
|
|
|
|
|
btn = self.wait_for(lambda: self.browser.find_element(By.ID, "id_tray_btn"))
|
|
|
|
|
|
self._simulate_drag_y(btn, 300)
|
|
|
|
|
|
self.wait_for(
|
|
|
|
|
|
lambda: self.assertTrue(self.browser.execute_script("return Tray.isOpen()"))
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
tray = self.browser.find_element(By.ID, "id_tray")
|
|
|
|
|
|
cells = self.browser.find_elements(By.CSS_SELECTOR, "#id_tray_grid .tray-cell")
|
|
|
|
|
|
self.assertEqual(len(cells), 8)
|
|
|
|
|
|
|
|
|
|
|
|
tray_right = tray.location['x'] + tray.size['width']
|
|
|
|
|
|
tray_bottom = tray.location['y'] + tray.size['height']
|
|
|
|
|
|
|
|
|
|
|
|
# Each cell must fit within the tray interior (2px rounding slack)
|
|
|
|
|
|
for cell in cells:
|
|
|
|
|
|
self.assertLessEqual(
|
|
|
|
|
|
cell.location['x'] + cell.size['width'], tray_right + 2,
|
|
|
|
|
|
msg="Cell overflows tray right edge"
|
|
|
|
|
|
)
|
|
|
|
|
|
self.assertLessEqual(
|
|
|
|
|
|
cell.location['y'] + cell.size['height'], tray_bottom + 2,
|
|
|
|
|
|
msg="Cell overflows tray bottom edge"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
#
|
|
|
|
|
|
# Tarot deck + Game Kit FTs — migrated from the legacy
|
|
|
|
|
|
# test_component_cards_tarot.py (2026-05-12). These exercise the in-room tarot
|
|
|
|
|
|
# deck page (Celtic Cross deal), the Game Kit deck-variant selection w. hover
|
|
|
|
|
|
# tooltips + Equip/Equipped state, and the dedicated game-kit page w. its
|
|
|
|
|
|
# four applet rows + tarot fan modal. The admin-side tarot browse FT split
|
|
|
|
|
|
# off into test_admin_tarot.py at the same time.
|
|
|
|
|
|
#
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
2026-03-24 21:07:01 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TarotDeckTest(FunctionalTest):
|
|
|
|
|
|
"""A room founder can view the tarot deck page and deal a Celtic Cross spread."""
|
|
|
|
|
|
|
|
|
|
|
|
def setUp(self):
|
|
|
|
|
|
super().setUp()
|
|
|
|
|
|
# DeckVariant + TarotCard rows are flushed by TransactionTestCase — recreate
|
|
|
|
|
|
from apps.epic.models import TarotCard
|
|
|
|
|
|
self.earthman, _ = DeckVariant.objects.get_or_create(
|
|
|
|
|
|
slug="earthman",
|
|
|
|
|
|
defaults={"name": "Earthman Deck", "card_count": 108, "is_default": True},
|
|
|
|
|
|
)
|
|
|
|
|
|
# Seed 8 major cards — enough for a 6-card cross deal (with buffer)
|
|
|
|
|
|
major_stubs = [
|
|
|
|
|
|
(0, "The Schiz", "the-schiz-ft"),
|
|
|
|
|
|
(1, "Pope I: President", "pope-i-president-ft"),
|
|
|
|
|
|
(2, "Pope II: Tsar", "pope-ii-tsar-ft"),
|
|
|
|
|
|
(3, "Pope III: Chairman","pope-iii-chairman-ft"),
|
|
|
|
|
|
(4, "Pope IV: Emperor", "pope-iv-emperor-ft"),
|
|
|
|
|
|
(5, "Pope V: Chancellor","pope-v-chancellor-ft"),
|
|
|
|
|
|
(10, "Wheel of Fortune", "wheel-of-fortune-em-ft"),
|
|
|
|
|
|
(11, "The Junkboat", "the-junkboat-ft"),
|
|
|
|
|
|
]
|
|
|
|
|
|
for number, name, slug in major_stubs:
|
|
|
|
|
|
TarotCard.objects.get_or_create(
|
|
|
|
|
|
deck_variant=self.earthman, slug=slug,
|
|
|
|
|
|
defaults={"name": name, "arcana": "MAJOR", "number": number},
|
|
|
|
|
|
)
|
|
|
|
|
|
self.founder = User.objects.create(email="founder@test.io")
|
|
|
|
|
|
# Signal sets equipped_deck to Earthman (now it exists)
|
|
|
|
|
|
self.founder.refresh_from_db()
|
|
|
|
|
|
self.room = Room.objects.create(name="Whispering Pines", owner=self.founder)
|
|
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
# Test 2 — tarot deck page reports 108 cards (Earthman default) #
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
|
|
|
|
|
|
def test_founder_can_reach_room_tarot_page_and_sees_full_deck(self):
|
|
|
|
|
|
self.create_pre_authenticated_session("founder@test.io")
|
|
|
|
|
|
self.browser.get(
|
|
|
|
|
|
self.live_server_url + f"/gameboard/room/{self.room.id}/tarot/"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Browser tab title confirms we're on the tarot page
|
|
|
|
|
|
self.wait_for(
|
|
|
|
|
|
lambda: self.assertIn("Tarot", self.browser.title)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Deck status shows all 108 Earthman cards remaining
|
|
|
|
|
|
status = self.browser.find_element(By.CSS_SELECTOR, "[data-tarot-remaining]")
|
|
|
|
|
|
self.assertEqual(status.get_attribute("data-tarot-remaining"), "108")
|
|
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
# Test 3 — dealing a Celtic Cross spread shows 10 positioned cards #
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
|
|
|
|
|
|
def test_dealing_celtic_cross_spread_shows_ten_unique_cards(self):
|
|
|
|
|
|
self.create_pre_authenticated_session("founder@test.io")
|
|
|
|
|
|
self.browser.get(
|
|
|
|
|
|
self.live_server_url + f"/gameboard/room/{self.room.id}/tarot/"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Click the "Deal Celtic Cross" button
|
|
|
|
|
|
self.wait_for(
|
|
|
|
|
|
lambda: self.browser.find_element(By.CSS_SELECTOR, "[data-deal-spread]")
|
|
|
|
|
|
).click()
|
|
|
|
|
|
|
|
|
|
|
|
# Six cross positions appear in the spread (staff positions filled via gameplay)
|
|
|
|
|
|
positions = self.wait_for(
|
|
|
|
|
|
lambda: self.browser.find_elements(By.CSS_SELECTOR, ".tarot-position")
|
|
|
|
|
|
)
|
|
|
|
|
|
self.assertEqual(len(positions), 6)
|
|
|
|
|
|
|
|
|
|
|
|
# Each position shows a card name and an orientation label
|
|
|
|
|
|
names = set()
|
|
|
|
|
|
for pos in positions:
|
|
|
|
|
|
name = pos.find_element(By.CSS_SELECTOR, ".tarot-card-name").text
|
|
|
|
|
|
orientation = pos.find_element(By.CSS_SELECTOR, ".tarot-card-orientation").text
|
|
|
|
|
|
self.assertTrue(len(name) > 0, "Card name should not be empty")
|
|
|
|
|
|
self.assertIn(orientation, ["Upright", "Reversed"])
|
|
|
|
|
|
names.add(name)
|
|
|
|
|
|
|
|
|
|
|
|
# All 6 cards are unique
|
|
|
|
|
|
self.assertEqual(len(names), 6, "All 6 drawn cards must be unique")
|
|
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
# Test 4 — deck count decreases after the spread is dealt #
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
|
|
|
|
|
|
def test_remaining_count_decreases_after_dealing_spread(self):
|
|
|
|
|
|
self.create_pre_authenticated_session("founder@test.io")
|
|
|
|
|
|
self.browser.get(
|
|
|
|
|
|
self.live_server_url + f"/gameboard/room/{self.room.id}/tarot/"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
self.wait_for(
|
|
|
|
|
|
lambda: self.browser.find_element(By.CSS_SELECTOR, "[data-deal-spread]")
|
|
|
|
|
|
).click()
|
|
|
|
|
|
|
|
|
|
|
|
# After dealing 6 cross cards from the 108-card Earthman deck, 102 remain
|
|
|
|
|
|
remaining = self.wait_for(
|
|
|
|
|
|
lambda: self.browser.find_element(By.CSS_SELECTOR, "[data-tarot-remaining]")
|
|
|
|
|
|
)
|
|
|
|
|
|
self.assertEqual(remaining.get_attribute("data-tarot-remaining"), "102")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class GameKitDeckSelectionTest(FunctionalTest):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Game Kit applet on gameboard shows available deck variants with hover
|
|
|
|
|
|
tooltips and an equip/equipped state — following the same mini-tooltip
|
|
|
|
|
|
pattern as trinket selection.
|
|
|
|
|
|
|
|
|
|
|
|
Test scenario: the gamer's active deck is explicitly set to Fiorentine
|
|
|
|
|
|
(non-default) in setUp, so we can exercise switching back to Earthman.
|
|
|
|
|
|
Once DeckVariant model exists, replace the TODO stubs with real ORM calls.
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
def setUp(self):
|
|
|
|
|
|
super().setUp()
|
|
|
|
|
|
for slug, name, cols, rows in [
|
|
|
|
|
|
("new-game", "New Game", 6, 3),
|
|
|
|
|
|
("my-games", "My Games", 6, 3),
|
|
|
|
|
|
("game-kit", "Game Kit", 6, 3),
|
|
|
|
|
|
]:
|
|
|
|
|
|
Applet.objects.get_or_create(
|
|
|
|
|
|
slug=slug,
|
|
|
|
|
|
defaults={
|
|
|
|
|
|
"name": name, "grid_cols": cols,
|
|
|
|
|
|
"grid_rows": rows, "context": "gameboard",
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
2026-03-24 21:52:57 -04:00
|
|
|
|
# DeckVariant rows are flushed by TransactionTestCase — recreate before
|
|
|
|
|
|
# creating the user so the post_save signal can set equipped_deck = earthman.
|
|
|
|
|
|
self.earthman, _ = DeckVariant.objects.get_or_create(
|
|
|
|
|
|
slug="earthman",
|
|
|
|
|
|
defaults={"name": "Earthman Deck", "card_count": 108, "is_default": True},
|
|
|
|
|
|
)
|
A.0 image-rendering schema + RWS rename + canonical-Earthman suit collapse — TDD. Sprint A.0 of [[project-image-based-deck-face-rendering]]. Adds three `DeckVariant` fields: `has_card_images` (BooleanField default=True — Earthman keeps False until its artwork ships, every new deck defaults True), `family` (CharField choices=[earthman, italian, english, playing] default=earthman — drives per-family display + filename slug mapping per [[reference-card-image-naming-convention]]), `is_polarized` (BooleanField default=False — Earthman is True today; Sprint A.4 game_kit applet will render "(×2)" in --terUser for polarized decks; Sprint C+B segment model uses it for segment-count logic). `TarotCard.SUIT_CHOICES` collapses from 8 values to 4 canonical Earthman values (BRANDS / CROWNS / GRAILS / BLADES); WANDS / CUPS / SWORDS / PENTACLES dropped — they were duplicative at the structural level since `sig_deck_cards` + `levity/gravity_sig_cards` already treated [WANDS, BRANDS, CROWNS] as one segment and [SWORDS, BLADES, CUPS, GRAILS] as another (so the project already *functionally* equated them; the lock just makes that explicit). Per-family display vocab (`batons` for Italian, `wands` for English, `clubs` for Playing) lives in Sprint A.2's `display_suit_name` property, not in the enum. Audit 2026-05-25 revealed the existing `fiorentine-minchiate` DeckVariant is actually 78-card RWS Tarot in disguise (22 majors numbered 0-21 w. RWS names: The Fool / The Magician / ... / The World; 56 minors in 4 suits × 14 cards) — NOT Minchiate (which has 40 trumps + 1 Il Matto + 56 minors = 97 cards). Migration 0012 renames the slug → `tarot-rider-waite-smith`, name → "Tarot (Rider-Waite-Smith)", sets family='english', has_card_images=False, is_polarized=False — and revocabs its 56 minor cards' suits in-place (WANDS→BRANDS, CUPS→GRAILS, SWORDS→BLADES, PENTACLES→CROWNS) so they match the new canonical enum. FKs (User.equipped_deck, User.unlocked_decks, TableSeat.deck_variant, etc.) survive untouched — slug-only changes don't break referential integrity. Earthman fields set explicitly in 0012 too (family=earthman, has_card_images=False, is_polarized=True). Companion code simplifications: `sig_deck_cards` + `_sig_unique_cards_for_deck` queries shrink from `suit__in=[3 values]` and `[4 values]` to `[2 values]` each (one per segment); `TarotCard.suit_icon` mapping shrinks from 8 entries to 4; `gameboard.views.tarot_fan._suit_order` shrinks from 8 keys to 4. Existing test files updated: `test_game_room_tray.py` (largest update — `self.fiorentine` → `self.rws`, `id_kit_fiorentine_deck` → `id_kit_tarot_deck` (template-id derives from deck.short_key = first slug segment), assertion "Fiorentine" → "Rider-Waite-Smith"); `test_game_room_deck_contrib.py` (same pattern, smaller); `lyric/test_models.py` + `gameboard/test_views.py` (slug literal swaps only); `epic/test_models.py` `_make_sig_card` test fixtures: "WANDS"→"BRANDS", "CUPS"→"GRAILS". 14 new ITs in `DeckSchemaA0Test` cover the schema additions + migration outcomes (field existence + choice values + earthman has all three fields set correctly + RWS rename verified + RWS cards use canonical suits + dropped enum values absent from SUIT_CHOICES). Tests: 14 new green; 1255/1255 IT+UT total green (38s); no regressions. Out of scope: Sprint A.1 will seed the actual Minchiate Fiorentine 1860-1890 (97-card) DeckVariant + TarotCard rows w. family='italian', has_card_images=True; A.2 adds the `image_filename` + `display_suit_name` properties that consume the new `family` field; A.3+ wires the render branches across 6 surfaces
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 23:25:26 -04:00
|
|
|
|
self.rws, _ = DeckVariant.objects.get_or_create(
|
|
|
|
|
|
slug="tarot-rider-waite-smith",
|
|
|
|
|
|
defaults={"name": "Tarot (Rider-Waite-Smith)", "card_count": 78, "is_default": False},
|
2026-03-24 21:52:57 -04:00
|
|
|
|
)
|
2026-03-24 21:07:01 -04:00
|
|
|
|
self.gamer = User.objects.create(email="gamer@deck.io")
|
2026-03-24 22:34:50 -04:00
|
|
|
|
# Signal sets equipped_deck = earthman and unlocked_decks = [earthman].
|
|
|
|
|
|
# Explicitly grant fiorentine too, then switch equipped_deck to it so
|
|
|
|
|
|
# the test can exercise switching back to Earthman.
|
2026-03-24 21:52:57 -04:00
|
|
|
|
self.gamer.refresh_from_db()
|
A.0 image-rendering schema + RWS rename + canonical-Earthman suit collapse — TDD. Sprint A.0 of [[project-image-based-deck-face-rendering]]. Adds three `DeckVariant` fields: `has_card_images` (BooleanField default=True — Earthman keeps False until its artwork ships, every new deck defaults True), `family` (CharField choices=[earthman, italian, english, playing] default=earthman — drives per-family display + filename slug mapping per [[reference-card-image-naming-convention]]), `is_polarized` (BooleanField default=False — Earthman is True today; Sprint A.4 game_kit applet will render "(×2)" in --terUser for polarized decks; Sprint C+B segment model uses it for segment-count logic). `TarotCard.SUIT_CHOICES` collapses from 8 values to 4 canonical Earthman values (BRANDS / CROWNS / GRAILS / BLADES); WANDS / CUPS / SWORDS / PENTACLES dropped — they were duplicative at the structural level since `sig_deck_cards` + `levity/gravity_sig_cards` already treated [WANDS, BRANDS, CROWNS] as one segment and [SWORDS, BLADES, CUPS, GRAILS] as another (so the project already *functionally* equated them; the lock just makes that explicit). Per-family display vocab (`batons` for Italian, `wands` for English, `clubs` for Playing) lives in Sprint A.2's `display_suit_name` property, not in the enum. Audit 2026-05-25 revealed the existing `fiorentine-minchiate` DeckVariant is actually 78-card RWS Tarot in disguise (22 majors numbered 0-21 w. RWS names: The Fool / The Magician / ... / The World; 56 minors in 4 suits × 14 cards) — NOT Minchiate (which has 40 trumps + 1 Il Matto + 56 minors = 97 cards). Migration 0012 renames the slug → `tarot-rider-waite-smith`, name → "Tarot (Rider-Waite-Smith)", sets family='english', has_card_images=False, is_polarized=False — and revocabs its 56 minor cards' suits in-place (WANDS→BRANDS, CUPS→GRAILS, SWORDS→BLADES, PENTACLES→CROWNS) so they match the new canonical enum. FKs (User.equipped_deck, User.unlocked_decks, TableSeat.deck_variant, etc.) survive untouched — slug-only changes don't break referential integrity. Earthman fields set explicitly in 0012 too (family=earthman, has_card_images=False, is_polarized=True). Companion code simplifications: `sig_deck_cards` + `_sig_unique_cards_for_deck` queries shrink from `suit__in=[3 values]` and `[4 values]` to `[2 values]` each (one per segment); `TarotCard.suit_icon` mapping shrinks from 8 entries to 4; `gameboard.views.tarot_fan._suit_order` shrinks from 8 keys to 4. Existing test files updated: `test_game_room_tray.py` (largest update — `self.fiorentine` → `self.rws`, `id_kit_fiorentine_deck` → `id_kit_tarot_deck` (template-id derives from deck.short_key = first slug segment), assertion "Fiorentine" → "Rider-Waite-Smith"); `test_game_room_deck_contrib.py` (same pattern, smaller); `lyric/test_models.py` + `gameboard/test_views.py` (slug literal swaps only); `epic/test_models.py` `_make_sig_card` test fixtures: "WANDS"→"BRANDS", "CUPS"→"GRAILS". 14 new ITs in `DeckSchemaA0Test` cover the schema additions + migration outcomes (field existence + choice values + earthman has all three fields set correctly + RWS rename verified + RWS cards use canonical suits + dropped enum values absent from SUIT_CHOICES). Tests: 14 new green; 1255/1255 IT+UT total green (38s); no regressions. Out of scope: Sprint A.1 will seed the actual Minchiate Fiorentine 1860-1890 (97-card) DeckVariant + TarotCard rows w. family='italian', has_card_images=True; A.2 adds the `image_filename` + `display_suit_name` properties that consume the new `family` field; A.3+ wires the render branches across 6 surfaces
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 23:25:26 -04:00
|
|
|
|
self.gamer.unlocked_decks.add(self.rws)
|
|
|
|
|
|
self.gamer.equipped_deck = self.rws
|
2026-03-24 21:52:57 -04:00
|
|
|
|
self.gamer.save(update_fields=["equipped_deck"])
|
2026-03-24 21:07:01 -04:00
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
# Test 5 — Game Kit shows deck cards with correct equip/equipped state #
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
|
|
|
|
|
|
def test_game_kit_deck_cards_show_equip_state_and_switching_works(self):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Gamer (currently on Fiorentine) visits gameboard, hovers over the
|
|
|
|
|
|
Earthman deck — sees it is NOT equipped. Hovers to Fiorentine — sees
|
|
|
|
|
|
it IS equipped. Hovers back to Earthman and clicks Equip.
|
|
|
|
|
|
"""
|
|
|
|
|
|
self.create_pre_authenticated_session("gamer@deck.io")
|
|
|
|
|
|
self.browser.get(self.live_server_url + "/gameboard/")
|
|
|
|
|
|
self.wait_for(lambda: self.browser.find_element(By.ID, "id_game_kit"))
|
|
|
|
|
|
|
|
|
|
|
|
# ── Hover over Earthman deck ──────────────────────────────────────
|
|
|
|
|
|
earthman_el = self.wait_for(
|
|
|
|
|
|
lambda: self.browser.find_element(By.ID, "id_kit_earthman_deck")
|
|
|
|
|
|
)
|
|
|
|
|
|
self.browser.execute_script(
|
|
|
|
|
|
"arguments[0].scrollIntoView({block: 'center'})", earthman_el
|
|
|
|
|
|
)
|
|
|
|
|
|
ActionChains(self.browser).move_to_element(earthman_el).perform()
|
|
|
|
|
|
|
|
|
|
|
|
# Main tooltip shows deck name and card count
|
|
|
|
|
|
self.wait_for(
|
|
|
|
|
|
lambda: self.browser.find_element(By.ID, "id_tooltip_portal").is_displayed()
|
|
|
|
|
|
)
|
|
|
|
|
|
portal = self.browser.find_element(By.ID, "id_tooltip_portal")
|
|
|
|
|
|
self.assertIn("Earthman", portal.text)
|
|
|
|
|
|
self.assertIn("108", portal.text)
|
|
|
|
|
|
|
2026-04-16 01:57:02 -04:00
|
|
|
|
# Mini shows "Not Equipped"; DON button is active in the main portal
|
2026-03-24 21:07:01 -04:00
|
|
|
|
mini = self.browser.find_element(By.ID, "id_mini_tooltip_portal")
|
|
|
|
|
|
self.wait_for(lambda: self.assertTrue(mini.is_displayed()))
|
2026-04-16 01:57:02 -04:00
|
|
|
|
self.assertIn("Not Equipped", mini.text)
|
|
|
|
|
|
don = portal.find_element(By.CSS_SELECTOR, ".btn-equip")
|
|
|
|
|
|
self.assertNotIn("btn-disabled", don.get_attribute("class"))
|
2026-03-24 21:07:01 -04:00
|
|
|
|
|
A.0 image-rendering schema + RWS rename + canonical-Earthman suit collapse — TDD. Sprint A.0 of [[project-image-based-deck-face-rendering]]. Adds three `DeckVariant` fields: `has_card_images` (BooleanField default=True — Earthman keeps False until its artwork ships, every new deck defaults True), `family` (CharField choices=[earthman, italian, english, playing] default=earthman — drives per-family display + filename slug mapping per [[reference-card-image-naming-convention]]), `is_polarized` (BooleanField default=False — Earthman is True today; Sprint A.4 game_kit applet will render "(×2)" in --terUser for polarized decks; Sprint C+B segment model uses it for segment-count logic). `TarotCard.SUIT_CHOICES` collapses from 8 values to 4 canonical Earthman values (BRANDS / CROWNS / GRAILS / BLADES); WANDS / CUPS / SWORDS / PENTACLES dropped — they were duplicative at the structural level since `sig_deck_cards` + `levity/gravity_sig_cards` already treated [WANDS, BRANDS, CROWNS] as one segment and [SWORDS, BLADES, CUPS, GRAILS] as another (so the project already *functionally* equated them; the lock just makes that explicit). Per-family display vocab (`batons` for Italian, `wands` for English, `clubs` for Playing) lives in Sprint A.2's `display_suit_name` property, not in the enum. Audit 2026-05-25 revealed the existing `fiorentine-minchiate` DeckVariant is actually 78-card RWS Tarot in disguise (22 majors numbered 0-21 w. RWS names: The Fool / The Magician / ... / The World; 56 minors in 4 suits × 14 cards) — NOT Minchiate (which has 40 trumps + 1 Il Matto + 56 minors = 97 cards). Migration 0012 renames the slug → `tarot-rider-waite-smith`, name → "Tarot (Rider-Waite-Smith)", sets family='english', has_card_images=False, is_polarized=False — and revocabs its 56 minor cards' suits in-place (WANDS→BRANDS, CUPS→GRAILS, SWORDS→BLADES, PENTACLES→CROWNS) so they match the new canonical enum. FKs (User.equipped_deck, User.unlocked_decks, TableSeat.deck_variant, etc.) survive untouched — slug-only changes don't break referential integrity. Earthman fields set explicitly in 0012 too (family=earthman, has_card_images=False, is_polarized=True). Companion code simplifications: `sig_deck_cards` + `_sig_unique_cards_for_deck` queries shrink from `suit__in=[3 values]` and `[4 values]` to `[2 values]` each (one per segment); `TarotCard.suit_icon` mapping shrinks from 8 entries to 4; `gameboard.views.tarot_fan._suit_order` shrinks from 8 keys to 4. Existing test files updated: `test_game_room_tray.py` (largest update — `self.fiorentine` → `self.rws`, `id_kit_fiorentine_deck` → `id_kit_tarot_deck` (template-id derives from deck.short_key = first slug segment), assertion "Fiorentine" → "Rider-Waite-Smith"); `test_game_room_deck_contrib.py` (same pattern, smaller); `lyric/test_models.py` + `gameboard/test_views.py` (slug literal swaps only); `epic/test_models.py` `_make_sig_card` test fixtures: "WANDS"→"BRANDS", "CUPS"→"GRAILS". 14 new ITs in `DeckSchemaA0Test` cover the schema additions + migration outcomes (field existence + choice values + earthman has all three fields set correctly + RWS rename verified + RWS cards use canonical suits + dropped enum values absent from SUIT_CHOICES). Tests: 14 new green; 1255/1255 IT+UT total green (38s); no regressions. Out of scope: Sprint A.1 will seed the actual Minchiate Fiorentine 1860-1890 (97-card) DeckVariant + TarotCard rows w. family='italian', has_card_images=True; A.2 adds the `image_filename` + `display_suit_name` properties that consume the new `family` field; A.3+ wires the render branches across 6 surfaces
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 23:25:26 -04:00
|
|
|
|
# ── Hover over Tarot (Rider-Waite-Smith) deck ────────────────────
|
|
|
|
|
|
rws_el = self.browser.find_element(By.ID, "id_kit_tarot_deck")
|
2026-03-24 21:07:01 -04:00
|
|
|
|
self.browser.execute_script(
|
A.0 image-rendering schema + RWS rename + canonical-Earthman suit collapse — TDD. Sprint A.0 of [[project-image-based-deck-face-rendering]]. Adds three `DeckVariant` fields: `has_card_images` (BooleanField default=True — Earthman keeps False until its artwork ships, every new deck defaults True), `family` (CharField choices=[earthman, italian, english, playing] default=earthman — drives per-family display + filename slug mapping per [[reference-card-image-naming-convention]]), `is_polarized` (BooleanField default=False — Earthman is True today; Sprint A.4 game_kit applet will render "(×2)" in --terUser for polarized decks; Sprint C+B segment model uses it for segment-count logic). `TarotCard.SUIT_CHOICES` collapses from 8 values to 4 canonical Earthman values (BRANDS / CROWNS / GRAILS / BLADES); WANDS / CUPS / SWORDS / PENTACLES dropped — they were duplicative at the structural level since `sig_deck_cards` + `levity/gravity_sig_cards` already treated [WANDS, BRANDS, CROWNS] as one segment and [SWORDS, BLADES, CUPS, GRAILS] as another (so the project already *functionally* equated them; the lock just makes that explicit). Per-family display vocab (`batons` for Italian, `wands` for English, `clubs` for Playing) lives in Sprint A.2's `display_suit_name` property, not in the enum. Audit 2026-05-25 revealed the existing `fiorentine-minchiate` DeckVariant is actually 78-card RWS Tarot in disguise (22 majors numbered 0-21 w. RWS names: The Fool / The Magician / ... / The World; 56 minors in 4 suits × 14 cards) — NOT Minchiate (which has 40 trumps + 1 Il Matto + 56 minors = 97 cards). Migration 0012 renames the slug → `tarot-rider-waite-smith`, name → "Tarot (Rider-Waite-Smith)", sets family='english', has_card_images=False, is_polarized=False — and revocabs its 56 minor cards' suits in-place (WANDS→BRANDS, CUPS→GRAILS, SWORDS→BLADES, PENTACLES→CROWNS) so they match the new canonical enum. FKs (User.equipped_deck, User.unlocked_decks, TableSeat.deck_variant, etc.) survive untouched — slug-only changes don't break referential integrity. Earthman fields set explicitly in 0012 too (family=earthman, has_card_images=False, is_polarized=True). Companion code simplifications: `sig_deck_cards` + `_sig_unique_cards_for_deck` queries shrink from `suit__in=[3 values]` and `[4 values]` to `[2 values]` each (one per segment); `TarotCard.suit_icon` mapping shrinks from 8 entries to 4; `gameboard.views.tarot_fan._suit_order` shrinks from 8 keys to 4. Existing test files updated: `test_game_room_tray.py` (largest update — `self.fiorentine` → `self.rws`, `id_kit_fiorentine_deck` → `id_kit_tarot_deck` (template-id derives from deck.short_key = first slug segment), assertion "Fiorentine" → "Rider-Waite-Smith"); `test_game_room_deck_contrib.py` (same pattern, smaller); `lyric/test_models.py` + `gameboard/test_views.py` (slug literal swaps only); `epic/test_models.py` `_make_sig_card` test fixtures: "WANDS"→"BRANDS", "CUPS"→"GRAILS". 14 new ITs in `DeckSchemaA0Test` cover the schema additions + migration outcomes (field existence + choice values + earthman has all three fields set correctly + RWS rename verified + RWS cards use canonical suits + dropped enum values absent from SUIT_CHOICES). Tests: 14 new green; 1255/1255 IT+UT total green (38s); no regressions. Out of scope: Sprint A.1 will seed the actual Minchiate Fiorentine 1860-1890 (97-card) DeckVariant + TarotCard rows w. family='italian', has_card_images=True; A.2 adds the `image_filename` + `display_suit_name` properties that consume the new `family` field; A.3+ wires the render branches across 6 surfaces
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 23:25:26 -04:00
|
|
|
|
"arguments[0].scrollIntoView({block: 'center'})", rws_el
|
2026-03-24 21:07:01 -04:00
|
|
|
|
)
|
A.0 image-rendering schema + RWS rename + canonical-Earthman suit collapse — TDD. Sprint A.0 of [[project-image-based-deck-face-rendering]]. Adds three `DeckVariant` fields: `has_card_images` (BooleanField default=True — Earthman keeps False until its artwork ships, every new deck defaults True), `family` (CharField choices=[earthman, italian, english, playing] default=earthman — drives per-family display + filename slug mapping per [[reference-card-image-naming-convention]]), `is_polarized` (BooleanField default=False — Earthman is True today; Sprint A.4 game_kit applet will render "(×2)" in --terUser for polarized decks; Sprint C+B segment model uses it for segment-count logic). `TarotCard.SUIT_CHOICES` collapses from 8 values to 4 canonical Earthman values (BRANDS / CROWNS / GRAILS / BLADES); WANDS / CUPS / SWORDS / PENTACLES dropped — they were duplicative at the structural level since `sig_deck_cards` + `levity/gravity_sig_cards` already treated [WANDS, BRANDS, CROWNS] as one segment and [SWORDS, BLADES, CUPS, GRAILS] as another (so the project already *functionally* equated them; the lock just makes that explicit). Per-family display vocab (`batons` for Italian, `wands` for English, `clubs` for Playing) lives in Sprint A.2's `display_suit_name` property, not in the enum. Audit 2026-05-25 revealed the existing `fiorentine-minchiate` DeckVariant is actually 78-card RWS Tarot in disguise (22 majors numbered 0-21 w. RWS names: The Fool / The Magician / ... / The World; 56 minors in 4 suits × 14 cards) — NOT Minchiate (which has 40 trumps + 1 Il Matto + 56 minors = 97 cards). Migration 0012 renames the slug → `tarot-rider-waite-smith`, name → "Tarot (Rider-Waite-Smith)", sets family='english', has_card_images=False, is_polarized=False — and revocabs its 56 minor cards' suits in-place (WANDS→BRANDS, CUPS→GRAILS, SWORDS→BLADES, PENTACLES→CROWNS) so they match the new canonical enum. FKs (User.equipped_deck, User.unlocked_decks, TableSeat.deck_variant, etc.) survive untouched — slug-only changes don't break referential integrity. Earthman fields set explicitly in 0012 too (family=earthman, has_card_images=False, is_polarized=True). Companion code simplifications: `sig_deck_cards` + `_sig_unique_cards_for_deck` queries shrink from `suit__in=[3 values]` and `[4 values]` to `[2 values]` each (one per segment); `TarotCard.suit_icon` mapping shrinks from 8 entries to 4; `gameboard.views.tarot_fan._suit_order` shrinks from 8 keys to 4. Existing test files updated: `test_game_room_tray.py` (largest update — `self.fiorentine` → `self.rws`, `id_kit_fiorentine_deck` → `id_kit_tarot_deck` (template-id derives from deck.short_key = first slug segment), assertion "Fiorentine" → "Rider-Waite-Smith"); `test_game_room_deck_contrib.py` (same pattern, smaller); `lyric/test_models.py` + `gameboard/test_views.py` (slug literal swaps only); `epic/test_models.py` `_make_sig_card` test fixtures: "WANDS"→"BRANDS", "CUPS"→"GRAILS". 14 new ITs in `DeckSchemaA0Test` cover the schema additions + migration outcomes (field existence + choice values + earthman has all three fields set correctly + RWS rename verified + RWS cards use canonical suits + dropped enum values absent from SUIT_CHOICES). Tests: 14 new green; 1255/1255 IT+UT total green (38s); no regressions. Out of scope: Sprint A.1 will seed the actual Minchiate Fiorentine 1860-1890 (97-card) DeckVariant + TarotCard rows w. family='italian', has_card_images=True; A.2 adds the `image_filename` + `display_suit_name` properties that consume the new `family` field; A.3+ wires the render branches across 6 surfaces
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 23:25:26 -04:00
|
|
|
|
ActionChains(self.browser).move_to_element(rws_el).perform()
|
2026-03-24 21:07:01 -04:00
|
|
|
|
|
|
|
|
|
|
self.wait_for(
|
|
|
|
|
|
lambda: self.assertIn(
|
A.0 image-rendering schema + RWS rename + canonical-Earthman suit collapse — TDD. Sprint A.0 of [[project-image-based-deck-face-rendering]]. Adds three `DeckVariant` fields: `has_card_images` (BooleanField default=True — Earthman keeps False until its artwork ships, every new deck defaults True), `family` (CharField choices=[earthman, italian, english, playing] default=earthman — drives per-family display + filename slug mapping per [[reference-card-image-naming-convention]]), `is_polarized` (BooleanField default=False — Earthman is True today; Sprint A.4 game_kit applet will render "(×2)" in --terUser for polarized decks; Sprint C+B segment model uses it for segment-count logic). `TarotCard.SUIT_CHOICES` collapses from 8 values to 4 canonical Earthman values (BRANDS / CROWNS / GRAILS / BLADES); WANDS / CUPS / SWORDS / PENTACLES dropped — they were duplicative at the structural level since `sig_deck_cards` + `levity/gravity_sig_cards` already treated [WANDS, BRANDS, CROWNS] as one segment and [SWORDS, BLADES, CUPS, GRAILS] as another (so the project already *functionally* equated them; the lock just makes that explicit). Per-family display vocab (`batons` for Italian, `wands` for English, `clubs` for Playing) lives in Sprint A.2's `display_suit_name` property, not in the enum. Audit 2026-05-25 revealed the existing `fiorentine-minchiate` DeckVariant is actually 78-card RWS Tarot in disguise (22 majors numbered 0-21 w. RWS names: The Fool / The Magician / ... / The World; 56 minors in 4 suits × 14 cards) — NOT Minchiate (which has 40 trumps + 1 Il Matto + 56 minors = 97 cards). Migration 0012 renames the slug → `tarot-rider-waite-smith`, name → "Tarot (Rider-Waite-Smith)", sets family='english', has_card_images=False, is_polarized=False — and revocabs its 56 minor cards' suits in-place (WANDS→BRANDS, CUPS→GRAILS, SWORDS→BLADES, PENTACLES→CROWNS) so they match the new canonical enum. FKs (User.equipped_deck, User.unlocked_decks, TableSeat.deck_variant, etc.) survive untouched — slug-only changes don't break referential integrity. Earthman fields set explicitly in 0012 too (family=earthman, has_card_images=False, is_polarized=True). Companion code simplifications: `sig_deck_cards` + `_sig_unique_cards_for_deck` queries shrink from `suit__in=[3 values]` and `[4 values]` to `[2 values]` each (one per segment); `TarotCard.suit_icon` mapping shrinks from 8 entries to 4; `gameboard.views.tarot_fan._suit_order` shrinks from 8 keys to 4. Existing test files updated: `test_game_room_tray.py` (largest update — `self.fiorentine` → `self.rws`, `id_kit_fiorentine_deck` → `id_kit_tarot_deck` (template-id derives from deck.short_key = first slug segment), assertion "Fiorentine" → "Rider-Waite-Smith"); `test_game_room_deck_contrib.py` (same pattern, smaller); `lyric/test_models.py` + `gameboard/test_views.py` (slug literal swaps only); `epic/test_models.py` `_make_sig_card` test fixtures: "WANDS"→"BRANDS", "CUPS"→"GRAILS". 14 new ITs in `DeckSchemaA0Test` cover the schema additions + migration outcomes (field existence + choice values + earthman has all three fields set correctly + RWS rename verified + RWS cards use canonical suits + dropped enum values absent from SUIT_CHOICES). Tests: 14 new green; 1255/1255 IT+UT total green (38s); no regressions. Out of scope: Sprint A.1 will seed the actual Minchiate Fiorentine 1860-1890 (97-card) DeckVariant + TarotCard rows w. family='italian', has_card_images=True; A.2 adds the `image_filename` + `display_suit_name` properties that consume the new `family` field; A.3+ wires the render branches across 6 surfaces
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 23:25:26 -04:00
|
|
|
|
"Rider-Waite-Smith",
|
2026-03-24 21:07:01 -04:00
|
|
|
|
self.browser.find_element(By.ID, "id_tooltip_portal").text,
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
portal = self.browser.find_element(By.ID, "id_tooltip_portal")
|
|
|
|
|
|
self.assertIn("78", portal.text)
|
|
|
|
|
|
|
A.0 image-rendering schema + RWS rename + canonical-Earthman suit collapse — TDD. Sprint A.0 of [[project-image-based-deck-face-rendering]]. Adds three `DeckVariant` fields: `has_card_images` (BooleanField default=True — Earthman keeps False until its artwork ships, every new deck defaults True), `family` (CharField choices=[earthman, italian, english, playing] default=earthman — drives per-family display + filename slug mapping per [[reference-card-image-naming-convention]]), `is_polarized` (BooleanField default=False — Earthman is True today; Sprint A.4 game_kit applet will render "(×2)" in --terUser for polarized decks; Sprint C+B segment model uses it for segment-count logic). `TarotCard.SUIT_CHOICES` collapses from 8 values to 4 canonical Earthman values (BRANDS / CROWNS / GRAILS / BLADES); WANDS / CUPS / SWORDS / PENTACLES dropped — they were duplicative at the structural level since `sig_deck_cards` + `levity/gravity_sig_cards` already treated [WANDS, BRANDS, CROWNS] as one segment and [SWORDS, BLADES, CUPS, GRAILS] as another (so the project already *functionally* equated them; the lock just makes that explicit). Per-family display vocab (`batons` for Italian, `wands` for English, `clubs` for Playing) lives in Sprint A.2's `display_suit_name` property, not in the enum. Audit 2026-05-25 revealed the existing `fiorentine-minchiate` DeckVariant is actually 78-card RWS Tarot in disguise (22 majors numbered 0-21 w. RWS names: The Fool / The Magician / ... / The World; 56 minors in 4 suits × 14 cards) — NOT Minchiate (which has 40 trumps + 1 Il Matto + 56 minors = 97 cards). Migration 0012 renames the slug → `tarot-rider-waite-smith`, name → "Tarot (Rider-Waite-Smith)", sets family='english', has_card_images=False, is_polarized=False — and revocabs its 56 minor cards' suits in-place (WANDS→BRANDS, CUPS→GRAILS, SWORDS→BLADES, PENTACLES→CROWNS) so they match the new canonical enum. FKs (User.equipped_deck, User.unlocked_decks, TableSeat.deck_variant, etc.) survive untouched — slug-only changes don't break referential integrity. Earthman fields set explicitly in 0012 too (family=earthman, has_card_images=False, is_polarized=True). Companion code simplifications: `sig_deck_cards` + `_sig_unique_cards_for_deck` queries shrink from `suit__in=[3 values]` and `[4 values]` to `[2 values]` each (one per segment); `TarotCard.suit_icon` mapping shrinks from 8 entries to 4; `gameboard.views.tarot_fan._suit_order` shrinks from 8 keys to 4. Existing test files updated: `test_game_room_tray.py` (largest update — `self.fiorentine` → `self.rws`, `id_kit_fiorentine_deck` → `id_kit_tarot_deck` (template-id derives from deck.short_key = first slug segment), assertion "Fiorentine" → "Rider-Waite-Smith"); `test_game_room_deck_contrib.py` (same pattern, smaller); `lyric/test_models.py` + `gameboard/test_views.py` (slug literal swaps only); `epic/test_models.py` `_make_sig_card` test fixtures: "WANDS"→"BRANDS", "CUPS"→"GRAILS". 14 new ITs in `DeckSchemaA0Test` cover the schema additions + migration outcomes (field existence + choice values + earthman has all three fields set correctly + RWS rename verified + RWS cards use canonical suits + dropped enum values absent from SUIT_CHOICES). Tests: 14 new green; 1255/1255 IT+UT total green (38s); no regressions. Out of scope: Sprint A.1 will seed the actual Minchiate Fiorentine 1860-1890 (97-card) DeckVariant + TarotCard rows w. family='italian', has_card_images=True; A.2 adds the `image_filename` + `display_suit_name` properties that consume the new `family` field; A.3+ wires the render branches across 6 surfaces
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 23:25:26 -04:00
|
|
|
|
# Mini tooltip shows "Equipped" — RWS is the active deck
|
2026-03-24 21:07:01 -04:00
|
|
|
|
mini = self.browser.find_element(By.ID, "id_mini_tooltip_portal")
|
|
|
|
|
|
self.wait_for(lambda: self.assertTrue(mini.is_displayed()))
|
|
|
|
|
|
self.assertIn("Equipped", mini.text)
|
|
|
|
|
|
|
2026-04-16 01:57:02 -04:00
|
|
|
|
# ── Hover back to Earthman and click DON ─────────────────────────
|
2026-03-24 21:07:01 -04:00
|
|
|
|
ActionChains(self.browser).move_to_element(earthman_el).perform()
|
|
|
|
|
|
self.wait_for(
|
|
|
|
|
|
lambda: self.assertIn(
|
|
|
|
|
|
"Earthman",
|
|
|
|
|
|
self.browser.find_element(By.ID, "id_tooltip_portal").text,
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
2026-04-16 01:57:02 -04:00
|
|
|
|
portal = self.browser.find_element(By.ID, "id_tooltip_portal")
|
2026-03-24 21:07:01 -04:00
|
|
|
|
mini = self.browser.find_element(By.ID, "id_mini_tooltip_portal")
|
|
|
|
|
|
self.wait_for(lambda: self.assertTrue(mini.is_displayed()))
|
2026-04-16 01:57:02 -04:00
|
|
|
|
portal.find_element(By.CSS_SELECTOR, ".btn-equip").click()
|
2026-03-24 21:07:01 -04:00
|
|
|
|
|
2026-04-16 01:57:02 -04:00
|
|
|
|
# DON becomes disabled; mini updates to "Equipped"; data attr set optimistically
|
2026-03-24 21:07:01 -04:00
|
|
|
|
self.wait_for(
|
2026-04-16 01:57:02 -04:00
|
|
|
|
lambda: self.assertIn(
|
|
|
|
|
|
"btn-disabled",
|
|
|
|
|
|
portal.find_element(By.CSS_SELECTOR, ".btn-equip").get_attribute("class"),
|
2026-03-24 21:07:01 -04:00
|
|
|
|
)
|
|
|
|
|
|
)
|
2026-04-16 01:57:02 -04:00
|
|
|
|
self.assertIn("Equipped", self.browser.find_element(By.ID, "id_mini_tooltip_portal").text)
|
2026-03-24 21:07:01 -04:00
|
|
|
|
game_kit = self.browser.find_element(By.ID, "id_game_kit")
|
2026-04-16 01:57:02 -04:00
|
|
|
|
self.assertNotEqual(game_kit.get_attribute("data-equipped-deck-id"), "")
|
2026-03-24 22:34:50 -04:00
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
# Test 6 — new user's Game Kit shows only the default Earthman deck #
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
|
|
|
|
|
|
def test_new_user_game_kit_shows_only_earthman_deck(self):
|
|
|
|
|
|
"""A fresh user's game kit contains only the Earthman deck card;
|
|
|
|
|
|
the Fiorentine deck is not visible because it has not been unlocked."""
|
|
|
|
|
|
newcomer = User.objects.create(email="newcomer@deck.io")
|
|
|
|
|
|
newcomer.unlocked_decks.add(self.earthman)
|
|
|
|
|
|
self.create_pre_authenticated_session("newcomer@deck.io")
|
|
|
|
|
|
self.browser.get(self.live_server_url + "/gameboard/")
|
|
|
|
|
|
self.wait_for(lambda: self.browser.find_element(By.ID, "id_game_kit"))
|
|
|
|
|
|
|
|
|
|
|
|
deck_cards = self.browser.find_elements(By.CSS_SELECTOR, "#id_game_kit .deck-variant")
|
|
|
|
|
|
self.assertEqual(len(deck_cards), 1)
|
|
|
|
|
|
self.browser.find_element(By.ID, "id_kit_earthman_deck")
|
A.0 image-rendering schema + RWS rename + canonical-Earthman suit collapse — TDD. Sprint A.0 of [[project-image-based-deck-face-rendering]]. Adds three `DeckVariant` fields: `has_card_images` (BooleanField default=True — Earthman keeps False until its artwork ships, every new deck defaults True), `family` (CharField choices=[earthman, italian, english, playing] default=earthman — drives per-family display + filename slug mapping per [[reference-card-image-naming-convention]]), `is_polarized` (BooleanField default=False — Earthman is True today; Sprint A.4 game_kit applet will render "(×2)" in --terUser for polarized decks; Sprint C+B segment model uses it for segment-count logic). `TarotCard.SUIT_CHOICES` collapses from 8 values to 4 canonical Earthman values (BRANDS / CROWNS / GRAILS / BLADES); WANDS / CUPS / SWORDS / PENTACLES dropped — they were duplicative at the structural level since `sig_deck_cards` + `levity/gravity_sig_cards` already treated [WANDS, BRANDS, CROWNS] as one segment and [SWORDS, BLADES, CUPS, GRAILS] as another (so the project already *functionally* equated them; the lock just makes that explicit). Per-family display vocab (`batons` for Italian, `wands` for English, `clubs` for Playing) lives in Sprint A.2's `display_suit_name` property, not in the enum. Audit 2026-05-25 revealed the existing `fiorentine-minchiate` DeckVariant is actually 78-card RWS Tarot in disguise (22 majors numbered 0-21 w. RWS names: The Fool / The Magician / ... / The World; 56 minors in 4 suits × 14 cards) — NOT Minchiate (which has 40 trumps + 1 Il Matto + 56 minors = 97 cards). Migration 0012 renames the slug → `tarot-rider-waite-smith`, name → "Tarot (Rider-Waite-Smith)", sets family='english', has_card_images=False, is_polarized=False — and revocabs its 56 minor cards' suits in-place (WANDS→BRANDS, CUPS→GRAILS, SWORDS→BLADES, PENTACLES→CROWNS) so they match the new canonical enum. FKs (User.equipped_deck, User.unlocked_decks, TableSeat.deck_variant, etc.) survive untouched — slug-only changes don't break referential integrity. Earthman fields set explicitly in 0012 too (family=earthman, has_card_images=False, is_polarized=True). Companion code simplifications: `sig_deck_cards` + `_sig_unique_cards_for_deck` queries shrink from `suit__in=[3 values]` and `[4 values]` to `[2 values]` each (one per segment); `TarotCard.suit_icon` mapping shrinks from 8 entries to 4; `gameboard.views.tarot_fan._suit_order` shrinks from 8 keys to 4. Existing test files updated: `test_game_room_tray.py` (largest update — `self.fiorentine` → `self.rws`, `id_kit_fiorentine_deck` → `id_kit_tarot_deck` (template-id derives from deck.short_key = first slug segment), assertion "Fiorentine" → "Rider-Waite-Smith"); `test_game_room_deck_contrib.py` (same pattern, smaller); `lyric/test_models.py` + `gameboard/test_views.py` (slug literal swaps only); `epic/test_models.py` `_make_sig_card` test fixtures: "WANDS"→"BRANDS", "CUPS"→"GRAILS". 14 new ITs in `DeckSchemaA0Test` cover the schema additions + migration outcomes (field existence + choice values + earthman has all three fields set correctly + RWS rename verified + RWS cards use canonical suits + dropped enum values absent from SUIT_CHOICES). Tests: 14 new green; 1255/1255 IT+UT total green (38s); no regressions. Out of scope: Sprint A.1 will seed the actual Minchiate Fiorentine 1860-1890 (97-card) DeckVariant + TarotCard rows w. family='italian', has_card_images=True; A.2 adds the `image_filename` + `display_suit_name` properties that consume the new `family` field; A.3+ wires the render branches across 6 surfaces
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 23:25:26 -04:00
|
|
|
|
rws_cards = self.browser.find_elements(By.ID, "id_kit_tarot_deck")
|
|
|
|
|
|
self.assertEqual(len(rws_cards), 0)
|
2026-03-24 22:57:12 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class GameKitPageTest(FunctionalTest):
|
|
|
|
|
|
"""
|
|
|
|
|
|
User navigates from gameboard to the dedicated game-kit page.
|
|
|
|
|
|
The page shows four rows: trinkets, tokens, card decks, dice placeholder.
|
|
|
|
|
|
Clicking a deck card opens a tarot fan modal with coverflow navigation.
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
def setUp(self):
|
|
|
|
|
|
super().setUp()
|
|
|
|
|
|
from apps.epic.models import TarotCard
|
|
|
|
|
|
for slug, name, cols, rows in [
|
|
|
|
|
|
("new-game", "New Game", 6, 3),
|
|
|
|
|
|
("my-games", "My Games", 6, 3),
|
|
|
|
|
|
("game-kit", "Game Kit", 6, 3),
|
|
|
|
|
|
]:
|
|
|
|
|
|
Applet.objects.get_or_create(
|
|
|
|
|
|
slug=slug,
|
|
|
|
|
|
defaults={"name": name, "grid_cols": cols, "grid_rows": rows, "context": "gameboard"},
|
|
|
|
|
|
)
|
2026-04-04 14:33:35 -04:00
|
|
|
|
for slug, name in [
|
|
|
|
|
|
("gk-trinkets", "Trinkets"),
|
|
|
|
|
|
("gk-tokens", "Tokens"),
|
|
|
|
|
|
("gk-decks", "Card Decks"),
|
|
|
|
|
|
("gk-dice", "Dice Sets"),
|
|
|
|
|
|
]:
|
|
|
|
|
|
Applet.objects.get_or_create(
|
|
|
|
|
|
slug=slug,
|
|
|
|
|
|
defaults={"name": name, "grid_cols": 3, "grid_rows": 3, "context": "game-kit"},
|
|
|
|
|
|
)
|
2026-03-24 22:57:12 -04:00
|
|
|
|
self.earthman, _ = DeckVariant.objects.get_or_create(
|
|
|
|
|
|
slug="earthman",
|
|
|
|
|
|
defaults={"name": "Earthman Deck", "card_count": 108, "is_default": True},
|
|
|
|
|
|
)
|
|
|
|
|
|
# Seed 10 cards — enough to demonstrate full 7-card coverflow
|
|
|
|
|
|
for i in range(10):
|
|
|
|
|
|
TarotCard.objects.get_or_create(
|
|
|
|
|
|
deck_variant=self.earthman,
|
|
|
|
|
|
slug=f"gkp-card-{i}",
|
|
|
|
|
|
defaults={"name": f"Card {i}", "arcana": "MAJOR", "number": i},
|
|
|
|
|
|
)
|
|
|
|
|
|
# Create user after decks so signal sets equipped_deck + unlocked_decks
|
|
|
|
|
|
self.gamer = User.objects.create(email="gamer@kit.io")
|
|
|
|
|
|
self.gamer.refresh_from_db()
|
|
|
|
|
|
self.create_pre_authenticated_session("gamer@kit.io")
|
|
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
# Test 7 — gameboard Game Kit heading links to dedicated page #
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
|
|
|
|
|
|
def test_gameboard_game_kit_heading_links_to_game_kit_page(self):
|
|
|
|
|
|
self.browser.get(self.live_server_url + "/gameboard/")
|
|
|
|
|
|
link = self.wait_for(
|
|
|
|
|
|
lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_applet_game_kit h2 a")
|
|
|
|
|
|
)
|
|
|
|
|
|
link.click()
|
|
|
|
|
|
self.wait_for(lambda: self.assertIn("/gameboard/game-kit/", self.browser.current_url))
|
|
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
# Test 8 — game-kit page shows four rows #
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
|
|
|
|
|
|
def test_game_kit_page_shows_four_rows(self):
|
|
|
|
|
|
self.browser.get(self.live_server_url + "/gameboard/game-kit/")
|
|
|
|
|
|
self.wait_for(lambda: self.browser.find_element(By.ID, "id_gk_trinkets"))
|
|
|
|
|
|
self.browser.find_element(By.ID, "id_gk_tokens")
|
|
|
|
|
|
self.browser.find_element(By.ID, "id_gk_decks")
|
|
|
|
|
|
self.browser.find_element(By.ID, "id_gk_dice")
|
|
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
# Test 9 — clicking a deck card opens the tarot fan modal #
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
|
|
|
|
|
|
def test_clicking_deck_opens_tarot_fan_modal(self):
|
|
|
|
|
|
self.browser.get(self.live_server_url + "/gameboard/game-kit/")
|
|
|
|
|
|
self.wait_for(
|
|
|
|
|
|
lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_gk_decks .gk-deck-card")
|
|
|
|
|
|
).click()
|
|
|
|
|
|
dialog = self.browser.find_element(By.ID, "id_tarot_fan_dialog")
|
|
|
|
|
|
self.wait_for(lambda: self.assertTrue(dialog.is_displayed()))
|
|
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
# Test 10 — fan shows active center card plus receding cards #
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
|
|
|
|
|
|
def test_fan_shows_active_card_and_receding_cards(self):
|
|
|
|
|
|
self.browser.get(self.live_server_url + "/gameboard/game-kit/")
|
|
|
|
|
|
self.wait_for(
|
|
|
|
|
|
lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_gk_decks .gk-deck-card")
|
|
|
|
|
|
).click()
|
|
|
|
|
|
self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".fan-card--active"))
|
|
|
|
|
|
visible = self.browser.find_elements(
|
|
|
|
|
|
By.CSS_SELECTOR, "#id_fan_content .fan-card:not([style*='display: none'])"
|
|
|
|
|
|
)
|
|
|
|
|
|
self.assertGreater(len(visible), 1)
|
|
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
2026-04-05 23:33:13 -04:00
|
|
|
|
# Test 11 — clicking outside the modal closes it #
|
2026-03-24 22:57:12 -04:00
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
|
2026-03-25 00:05:52 -04:00
|
|
|
|
def test_pressing_escape_closes_fan_modal(self):
|
|
|
|
|
|
from selenium.webdriver.common.keys import Keys
|
2026-03-24 22:57:12 -04:00
|
|
|
|
self.browser.get(self.live_server_url + "/gameboard/game-kit/")
|
|
|
|
|
|
self.wait_for(
|
|
|
|
|
|
lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_gk_decks .gk-deck-card")
|
|
|
|
|
|
).click()
|
|
|
|
|
|
dialog = self.browser.find_element(By.ID, "id_tarot_fan_dialog")
|
|
|
|
|
|
self.wait_for(lambda: self.assertTrue(dialog.is_displayed()))
|
2026-03-25 00:05:52 -04:00
|
|
|
|
dialog.send_keys(Keys.ESCAPE)
|
2026-03-24 22:57:12 -04:00
|
|
|
|
self.wait_for(lambda: self.assertFalse(dialog.is_displayed()))
|