Compare commits
2 Commits
b97c4a0508
...
af1a90e76b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af1a90e76b | ||
|
|
8240de6b45 |
@@ -11,19 +11,21 @@ services:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: test-UTs-n-ITs
|
- name: test-UTs-n-ITs
|
||||||
image: python:3.13-slim
|
image: gitea.earthmanrpg.me/discoman/python-tdd-ci:latest
|
||||||
environment:
|
environment:
|
||||||
DATABASE_URL: postgresql://postgres:postgres@postgres/python_tdd_test
|
DATABASE_URL: postgresql://postgres:postgres@postgres/python_tdd_test
|
||||||
CELERY_BROKER_URL: redis://redis:6379/0
|
CELERY_BROKER_URL: redis://redis:6379/0
|
||||||
REDIS_URL: redis://redis:6379/1
|
REDIS_URL: redis://redis:6379/1
|
||||||
# Workspace-shared pip cache — Woodpecker mounts the workspace across
|
|
||||||
# all steps in a run, so the wheels populated here are reused by
|
|
||||||
# test-two-browser-FTs + test-FTs (saves ~30-60s × 2 = ~1-2 min per
|
|
||||||
# run). Cache lives only inside one run; cross-run caching would
|
|
||||||
# need a volume plugin.
|
|
||||||
PIP_CACHE_DIR: .pip-cache
|
PIP_CACHE_DIR: .pip-cache
|
||||||
commands:
|
commands:
|
||||||
- pip install -r requirements.txt
|
# `requirements.dev.txt` is the pinned superset Dockerfile.ci pre-
|
||||||
|
# installs; pinning here means pip skips resolver+download and just
|
||||||
|
# verifies "already satisfied" (~5-10s) instead of resolving unpinned
|
||||||
|
# requirements.txt against PyPI from scratch (~30-60s). Drift safety
|
||||||
|
# net: if requirements.dev.txt has changed since the CI image was
|
||||||
|
# last rebuilt + pushed, pip installs the delta — slower for that
|
||||||
|
# run but never broken. See TDD SKILL.md § CI dependency discipline.
|
||||||
|
- pip install -r requirements.dev.txt
|
||||||
- cd ./src
|
- cd ./src
|
||||||
- python manage.py test apps
|
- python manage.py test apps
|
||||||
when:
|
when:
|
||||||
@@ -45,7 +47,7 @@ steps:
|
|||||||
from_secret: stripe_publishable_key
|
from_secret: stripe_publishable_key
|
||||||
PIP_CACHE_DIR: .pip-cache
|
PIP_CACHE_DIR: .pip-cache
|
||||||
commands:
|
commands:
|
||||||
- pip install -r requirements.txt
|
- pip install -r requirements.dev.txt
|
||||||
- cd ./src
|
- cd ./src
|
||||||
- python manage.py collectstatic --noinput
|
- python manage.py collectstatic --noinput
|
||||||
- python manage.py test functional_tests --tag=two-browser
|
- python manage.py test functional_tests --tag=two-browser
|
||||||
@@ -70,7 +72,7 @@ steps:
|
|||||||
from_secret: stripe_publishable_key
|
from_secret: stripe_publishable_key
|
||||||
PIP_CACHE_DIR: .pip-cache
|
PIP_CACHE_DIR: .pip-cache
|
||||||
commands:
|
commands:
|
||||||
- pip install -r requirements.txt
|
- pip install -r requirements.dev.txt
|
||||||
- cd ./src
|
- cd ./src
|
||||||
- python manage.py collectstatic --noinput
|
- python manage.py collectstatic --noinput
|
||||||
- python manage.py test functional_tests --parallel --exclude-tag=channels --exclude-tag=two-browser
|
- python manage.py test functional_tests --parallel --exclude-tag=channels --exclude-tag=two-browser
|
||||||
|
|||||||
112
src/functional_tests/room_page.py
Normal file
112
src/functional_tests/room_page.py
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
"""Room-page FT helpers — extracted from the per-page FT modules so the
|
||||||
|
helpers can be imported without coupling consumer FTs to a sibling test
|
||||||
|
file's lifecycle (renames, deletions). Mirrors the post_page.py pattern.
|
||||||
|
|
||||||
|
Naming: underscored to signal "test infrastructure, not API surface", but
|
||||||
|
they're public within `functional_tests/` — direct imports are fine.
|
||||||
|
|
||||||
|
Consumers:
|
||||||
|
test_room_role_select / test_room_sig_select — define-and-use
|
||||||
|
test_component_tray_tooltip — _equip_earthman_deck
|
||||||
|
test_room_tray / test_deck_contribution — both core helpers
|
||||||
|
test_game_invite / test_room_gatekeeper — _fill_room_via_orm
|
||||||
|
"""
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from apps.drama.models import Note
|
||||||
|
from apps.epic.models import (
|
||||||
|
DeckVariant, GateSlot, Room, TableSeat, TarotCard,
|
||||||
|
)
|
||||||
|
from apps.lyric.models import User
|
||||||
|
|
||||||
|
|
||||||
|
SIG_SEAT_ORDER = ["PC", "NC", "EC", "SC", "AC", "BC"]
|
||||||
|
|
||||||
|
|
||||||
|
def _equip_earthman_deck(user):
|
||||||
|
# get_or_create: TransactionTestCase.flush() wipes migration-seeded
|
||||||
|
# DeckVariants between tests, so subsequent tests in the same run can't
|
||||||
|
# find it via filter().
|
||||||
|
deck, _ = DeckVariant.objects.get_or_create(
|
||||||
|
slug="earthman",
|
||||||
|
defaults={"name": "Earthman", "card_count": 106, "is_default": True},
|
||||||
|
)
|
||||||
|
user.equipped_deck = deck
|
||||||
|
user.save(update_fields=["equipped_deck"])
|
||||||
|
|
||||||
|
|
||||||
|
def _fill_room_via_orm(room, emails):
|
||||||
|
"""Fill all 6 gate slots and set gate_status=OPEN. Returns list of gamers."""
|
||||||
|
gamers = []
|
||||||
|
for i, email in enumerate(emails, start=1):
|
||||||
|
gamer, _ = User.objects.get_or_create(email=email)
|
||||||
|
slot = room.gate_slots.get(slot_number=i)
|
||||||
|
slot.gamer = gamer
|
||||||
|
slot.status = GateSlot.FILLED
|
||||||
|
slot.save()
|
||||||
|
gamers.append(gamer)
|
||||||
|
room.gate_status = Room.OPEN
|
||||||
|
room.save()
|
||||||
|
return gamers
|
||||||
|
|
||||||
|
|
||||||
|
def _assign_all_roles(room, role_order=None):
|
||||||
|
"""Assign roles to all slots, reveal them, and advance to SIG_SELECT.
|
||||||
|
Also ensures all gamers have an equipped_deck (required for sig_deck_cards)."""
|
||||||
|
if role_order is None:
|
||||||
|
role_order = SIG_SEAT_ORDER[:]
|
||||||
|
earthman, _ = DeckVariant.objects.get_or_create(
|
||||||
|
slug="earthman",
|
||||||
|
defaults={"name": "Earthman Deck", "card_count": 108, "is_default": True},
|
||||||
|
)
|
||||||
|
# Seed the 18 sig deck cards (migration data is flushed in TransactionTestCase FTs).
|
||||||
|
# _sig_unique_cards() filters arcana=MIDDLE, suits BRANDS/CROWNS/BLADES/GRAILS (Earthman).
|
||||||
|
_NAME = {11: "Maid", 12: "Jack", 13: "Queen", 14: "King"}
|
||||||
|
for suit in ("BRANDS", "CROWNS", "BLADES", "GRAILS"):
|
||||||
|
for number in (11, 12, 13, 14):
|
||||||
|
TarotCard.objects.get_or_create(
|
||||||
|
deck_variant=earthman,
|
||||||
|
slug=f"{_NAME[number].lower()}-of-{suit.lower()}-em",
|
||||||
|
defaults={"arcana": "MIDDLE", "suit": suit, "number": number,
|
||||||
|
"name": f"{_NAME[number]} of {suit.capitalize()}",
|
||||||
|
"levity_qualifier": "Elevated",
|
||||||
|
"gravity_qualifier": "Graven"},
|
||||||
|
)
|
||||||
|
# Numbers 0–1 are the sig deck's Major Arcana (unlocked via Note).
|
||||||
|
# Seed them with correct Earthman names and qualifiers, then unlock for all gamers.
|
||||||
|
for number, name, slug in [
|
||||||
|
(0, "The Nomad", "the-nomad"),
|
||||||
|
(1, "The Schizo", "the-schizo"),
|
||||||
|
]:
|
||||||
|
TarotCard.objects.get_or_create(
|
||||||
|
deck_variant=earthman,
|
||||||
|
slug=slug,
|
||||||
|
defaults={"arcana": "MAJOR", "number": number, "name": name,
|
||||||
|
"levity_qualifier": "Enlightened",
|
||||||
|
"gravity_qualifier": "Engraven"},
|
||||||
|
)
|
||||||
|
for slot in room.gate_slots.order_by("slot_number"):
|
||||||
|
if slot.gamer:
|
||||||
|
Note.objects.get_or_create(
|
||||||
|
user=slot.gamer, slug="super-nomad",
|
||||||
|
defaults={"earned_at": timezone.now()},
|
||||||
|
)
|
||||||
|
Note.objects.get_or_create(
|
||||||
|
user=slot.gamer, slug="super-schizo",
|
||||||
|
defaults={"earned_at": timezone.now()},
|
||||||
|
)
|
||||||
|
for slot in room.gate_slots.order_by("slot_number"):
|
||||||
|
if slot.gamer and not slot.gamer.equipped_deck:
|
||||||
|
slot.gamer.equipped_deck = earthman
|
||||||
|
slot.gamer.save(update_fields=["equipped_deck"])
|
||||||
|
TableSeat.objects.update_or_create(
|
||||||
|
room=room,
|
||||||
|
slot_number=slot.slot_number,
|
||||||
|
defaults={
|
||||||
|
"gamer": slot.gamer,
|
||||||
|
"role": role_order[slot.slot_number - 1],
|
||||||
|
"role_revealed": True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
room.table_status = Room.SIG_SELECT
|
||||||
|
room.save()
|
||||||
@@ -14,28 +14,11 @@ from selenium.webdriver.common.by import By
|
|||||||
|
|
||||||
from .base import FunctionalTest
|
from .base import FunctionalTest
|
||||||
from apps.applets.models import Applet
|
from apps.applets.models import Applet
|
||||||
from apps.epic.models import DeckVariant, GateSlot, Room, TableSeat, TarotCard
|
from apps.epic.models import DeckVariant, Room, TableSeat, TarotCard
|
||||||
from apps.lyric.models import User
|
from apps.lyric.models import User
|
||||||
|
|
||||||
|
|
||||||
def _equip_earthman_deck(user):
|
from .room_page import _equip_earthman_deck, _fill_room_via_orm
|
||||||
deck, _ = DeckVariant.objects.get_or_create(
|
|
||||||
slug="earthman",
|
|
||||||
defaults={"name": "Earthman", "card_count": 106, "is_default": True},
|
|
||||||
)
|
|
||||||
user.equipped_deck = deck
|
|
||||||
user.save(update_fields=["equipped_deck"])
|
|
||||||
|
|
||||||
|
|
||||||
def _fill_room_via_orm(room, emails):
|
|
||||||
for i, email in enumerate(emails, start=1):
|
|
||||||
gamer, _ = User.objects.get_or_create(email=email)
|
|
||||||
slot = room.gate_slots.get(slot_number=i)
|
|
||||||
slot.gamer = gamer
|
|
||||||
slot.status = GateSlot.FILLED
|
|
||||||
slot.save()
|
|
||||||
room.gate_status = Room.OPEN
|
|
||||||
room.save()
|
|
||||||
|
|
||||||
|
|
||||||
class TrayRoleCardTooltipTest(FunctionalTest):
|
class TrayRoleCardTooltipTest(FunctionalTest):
|
||||||
|
|||||||
@@ -22,8 +22,7 @@ from apps.applets.models import Applet
|
|||||||
from apps.epic.models import DeckVariant, GateSlot, Room, TableSeat
|
from apps.epic.models import DeckVariant, GateSlot, Room, TableSeat
|
||||||
from apps.lyric.models import User
|
from apps.lyric.models import User
|
||||||
from functional_tests.base import FunctionalTest
|
from functional_tests.base import FunctionalTest
|
||||||
from functional_tests.test_room_role_select import _fill_room_via_orm
|
from functional_tests.room_page import _fill_room_via_orm
|
||||||
from functional_tests.test_room_sig_select import _assign_all_roles
|
|
||||||
|
|
||||||
FOUNDER_EMAIL = "founder@test.io"
|
FOUNDER_EMAIL = "founder@test.io"
|
||||||
GAMER_EMAIL = "gamer@test.io"
|
GAMER_EMAIL = "gamer@test.io"
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ except ImportError:
|
|||||||
_BILLPOST_READY = False
|
_BILLPOST_READY = False
|
||||||
from apps.lyric.models import User
|
from apps.lyric.models import User
|
||||||
from functional_tests.base import FunctionalTest
|
from functional_tests.base import FunctionalTest
|
||||||
from functional_tests.test_room_role_select import _fill_room_via_orm
|
from functional_tests.room_page import _fill_room_via_orm
|
||||||
|
|
||||||
FOUNDER_EMAIL = "founder@test.io"
|
FOUNDER_EMAIL = "founder@test.io"
|
||||||
INVITEE_EMAIL = "invitee@test.io"
|
INVITEE_EMAIL = "invitee@test.io"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from .base import FunctionalTest
|
|||||||
from apps.applets.models import Applet
|
from apps.applets.models import Applet
|
||||||
from apps.epic.models import Room, GateSlot, select_token
|
from apps.epic.models import Room, GateSlot, select_token
|
||||||
from apps.lyric.models import Token, User
|
from apps.lyric.models import Token, User
|
||||||
from .test_room_role_select import _fill_room_via_orm
|
from .room_page import _fill_room_via_orm
|
||||||
|
|
||||||
|
|
||||||
class GatekeeperTest(FunctionalTest):
|
class GatekeeperTest(FunctionalTest):
|
||||||
|
|||||||
@@ -8,37 +8,12 @@ from selenium.webdriver.common.by import By
|
|||||||
|
|
||||||
from .base import FunctionalTest, ChannelsFunctionalTest
|
from .base import FunctionalTest, ChannelsFunctionalTest
|
||||||
from .management.commands.create_session import create_pre_authenticated_session
|
from .management.commands.create_session import create_pre_authenticated_session
|
||||||
|
from .room_page import _equip_earthman_deck, _fill_room_via_orm
|
||||||
from apps.applets.models import Applet
|
from apps.applets.models import Applet
|
||||||
from apps.epic.models import DeckVariant, Room, GateSlot, TableSeat
|
from apps.epic.models import Room, GateSlot, TableSeat
|
||||||
from apps.lyric.models import User
|
from apps.lyric.models import User
|
||||||
|
|
||||||
|
|
||||||
def _equip_earthman_deck(user):
|
|
||||||
# get_or_create: TransactionTestCase.flush() wipes migration-seeded DeckVariants
|
|
||||||
# between tests, so subsequent tests in the same run can't find it via filter().
|
|
||||||
deck, _ = DeckVariant.objects.get_or_create(
|
|
||||||
slug="earthman",
|
|
||||||
defaults={"name": "Earthman", "card_count": 106, "is_default": True},
|
|
||||||
)
|
|
||||||
user.equipped_deck = deck
|
|
||||||
user.save(update_fields=["equipped_deck"])
|
|
||||||
|
|
||||||
|
|
||||||
def _fill_room_via_orm(room, emails):
|
|
||||||
"""Fill all 6 gate slots and set gate_status=OPEN. Returns list of gamers."""
|
|
||||||
gamers = []
|
|
||||||
for i, email in enumerate(emails, start=1):
|
|
||||||
gamer, _ = User.objects.get_or_create(email=email)
|
|
||||||
slot = room.gate_slots.get(slot_number=i)
|
|
||||||
slot.gamer = gamer
|
|
||||||
slot.status = GateSlot.FILLED
|
|
||||||
slot.save()
|
|
||||||
gamers.append(gamer)
|
|
||||||
room.gate_status = Room.OPEN
|
|
||||||
room.save()
|
|
||||||
return gamers
|
|
||||||
|
|
||||||
|
|
||||||
class RoleSelectTest(FunctionalTest):
|
class RoleSelectTest(FunctionalTest):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ from selenium.webdriver.common.by import By
|
|||||||
|
|
||||||
from .base import FunctionalTest, ChannelsFunctionalTest
|
from .base import FunctionalTest, ChannelsFunctionalTest
|
||||||
from .management.commands.create_session import create_pre_authenticated_session
|
from .management.commands.create_session import create_pre_authenticated_session
|
||||||
|
from .room_page import (
|
||||||
|
SIG_SEAT_ORDER, _assign_all_roles, _fill_room_via_orm,
|
||||||
|
)
|
||||||
from apps.applets.models import Applet
|
from apps.applets.models import Applet
|
||||||
from apps.drama.models import Note
|
from apps.epic.models import Room
|
||||||
from apps.epic.models import DeckVariant, Room, TableSeat, TarotCard
|
|
||||||
from apps.lyric.models import User
|
from apps.lyric.models import User
|
||||||
|
|
||||||
from .test_room_role_select import _fill_room_via_orm
|
|
||||||
|
|
||||||
|
|
||||||
# ── Significator Selection ────────────────────────────────────────────────────
|
# ── Significator Selection ────────────────────────────────────────────────────
|
||||||
#
|
#
|
||||||
@@ -23,71 +23,6 @@ from .test_room_role_select import _fill_room_via_orm
|
|||||||
#
|
#
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
SIG_SEAT_ORDER = ["PC", "NC", "EC", "SC", "AC", "BC"]
|
|
||||||
|
|
||||||
|
|
||||||
def _assign_all_roles(room, role_order=None):
|
|
||||||
"""Assign roles to all slots, reveal them, and advance to SIG_SELECT.
|
|
||||||
Also ensures all gamers have an equipped_deck (required for sig_deck_cards)."""
|
|
||||||
if role_order is None:
|
|
||||||
role_order = SIG_SEAT_ORDER[:]
|
|
||||||
earthman, _ = DeckVariant.objects.get_or_create(
|
|
||||||
slug="earthman",
|
|
||||||
defaults={"name": "Earthman Deck", "card_count": 108, "is_default": True},
|
|
||||||
)
|
|
||||||
# Seed the 18 sig deck cards (migration data is flushed in TransactionTestCase FTs).
|
|
||||||
# _sig_unique_cards() filters arcana=MIDDLE, suits BRANDS/CROWNS/BLADES/GRAILS (Earthman).
|
|
||||||
_NAME = {11: "Maid", 12: "Jack", 13: "Queen", 14: "King"}
|
|
||||||
for suit in ("BRANDS", "CROWNS", "BLADES", "GRAILS"):
|
|
||||||
for number in (11, 12, 13, 14):
|
|
||||||
TarotCard.objects.get_or_create(
|
|
||||||
deck_variant=earthman,
|
|
||||||
slug=f"{_NAME[number].lower()}-of-{suit.lower()}-em",
|
|
||||||
defaults={"arcana": "MIDDLE", "suit": suit, "number": number,
|
|
||||||
"name": f"{_NAME[number]} of {suit.capitalize()}",
|
|
||||||
"levity_qualifier": "Elevated",
|
|
||||||
"gravity_qualifier": "Graven"},
|
|
||||||
)
|
|
||||||
# Numbers 0–1 are the sig deck's Major Arcana (unlocked via Note).
|
|
||||||
# Seed them with correct Earthman names and qualifiers, then unlock for all gamers.
|
|
||||||
from django.utils import timezone
|
|
||||||
for number, name, slug in [
|
|
||||||
(0, "The Nomad", "the-nomad"),
|
|
||||||
(1, "The Schizo", "the-schizo"),
|
|
||||||
]:
|
|
||||||
TarotCard.objects.get_or_create(
|
|
||||||
deck_variant=earthman,
|
|
||||||
slug=slug,
|
|
||||||
defaults={"arcana": "MAJOR", "number": number, "name": name,
|
|
||||||
"levity_qualifier": "Enlightened",
|
|
||||||
"gravity_qualifier": "Engraven"},
|
|
||||||
)
|
|
||||||
for slot in room.gate_slots.order_by("slot_number"):
|
|
||||||
if slot.gamer:
|
|
||||||
Note.objects.get_or_create(
|
|
||||||
user=slot.gamer, slug="super-nomad",
|
|
||||||
defaults={"earned_at": timezone.now()},
|
|
||||||
)
|
|
||||||
Note.objects.get_or_create(
|
|
||||||
user=slot.gamer, slug="super-schizo",
|
|
||||||
defaults={"earned_at": timezone.now()},
|
|
||||||
)
|
|
||||||
for slot in room.gate_slots.order_by("slot_number"):
|
|
||||||
if slot.gamer and not slot.gamer.equipped_deck:
|
|
||||||
slot.gamer.equipped_deck = earthman
|
|
||||||
slot.gamer.save(update_fields=["equipped_deck"])
|
|
||||||
TableSeat.objects.update_or_create(
|
|
||||||
room=room,
|
|
||||||
slot_number=slot.slot_number,
|
|
||||||
defaults={
|
|
||||||
"gamer": slot.gamer,
|
|
||||||
"role": role_order[slot.slot_number - 1],
|
|
||||||
"role_revealed": True,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
room.table_status = Room.SIG_SELECT
|
|
||||||
room.save()
|
|
||||||
|
|
||||||
|
|
||||||
class SigSelectTest(FunctionalTest):
|
class SigSelectTest(FunctionalTest):
|
||||||
"""Significator Selection — non-WebSocket tests."""
|
"""Significator Selection — non-WebSocket tests."""
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ from django.test import tag
|
|||||||
from selenium.webdriver.common.by import By
|
from selenium.webdriver.common.by import By
|
||||||
|
|
||||||
from .base import FunctionalTest
|
from .base import FunctionalTest
|
||||||
from .test_room_role_select import _fill_room_via_orm
|
from .room_page import _assign_all_roles, _fill_room_via_orm
|
||||||
from .test_room_sig_select import _assign_all_roles
|
|
||||||
from apps.epic.models import Room
|
from apps.epic.models import Room
|
||||||
from apps.lyric.models import User
|
from apps.lyric.models import User
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user