02798f8858
Form controls were styled "at a distance": Django renders bare <input>/<select>/<textarea>/<label>, so input.css reached in with ID-scoped #add-form descendant rules plus a global form *:disabled rule and .errorlist. The #add-form ID specificity forced state rules to climb, needed :not([data-search-select-search]) carve-outs, and broke on markup changes — it surfaced as the add_purchase Name/related_game fields not reading as disabled. Components now own all form styling via utilities on the elements themselves: - PrimitiveWidgetsMixin stamps INPUT/SELECT/TEXTAREA_CLASS (incl. disabled: variants) onto native widgets by type, skipping SearchSelect (self-styled) and checkboxes. - New FormFields(form, *, extras=...) renders label + control + errors + row layout with their own classes (replaces form.as_div()); the <form> owns its flex layout. extras appends a node into a named field's row (session timestamp buttons). - AddForm/purchase/session render via FormFields; login too — a new LoginForm(PrimitiveWidgetsMixin, AuthenticationForm) styles its inputs and auth.py renders it via FormFields + a StyledButton (was as_table). - input.css loses the entire #add-form block, the global :disabled rule, and .errorlist. State (disabled:) now lives on the element — no specificity wars, no carve-outs, robust to markup edits. Tests: error rendering uses the component class (not .errorlist); add-form labels/inputs carry their own classes; e2e login fixtures click the Login button by text (submit is now a <button>); Name disabled cursor asserted. CLAUDE.md documents the no-styling-at-a-distance + FormFields conventions. 513 passed; lint/format/ts-check clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
85 lines
3.1 KiB
Python
85 lines
3.1 KiB
Python
import pytest
|
|
from django.urls import reverse
|
|
from playwright.sync_api import Page, expect
|
|
|
|
|
|
@pytest.fixture
|
|
def authenticated_page(live_server, page: Page, django_user_model) -> Page:
|
|
django_user_model.objects.create_user(username="tester", password="secret123")
|
|
page.goto(f"{live_server.url}{reverse('login')}")
|
|
page.fill('input[name="username"]', "tester")
|
|
page.fill('input[name="password"]', "secret123")
|
|
page.click('button:has-text("Login")')
|
|
page.wait_for_url(f"{live_server.url}/tracker**")
|
|
return page
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_game_status_selector_opens_and_patches(authenticated_page: Page, live_server):
|
|
from games.models import Game, Platform
|
|
|
|
platform = Platform.objects.create(name="PC", icon="pc")
|
|
game = Game.objects.create(name="Test Game", platform=platform, status="u")
|
|
|
|
page = authenticated_page
|
|
page.goto(f"{live_server.url}{reverse('games:list_games')}")
|
|
|
|
host = page.locator("game-status-selector").first
|
|
expect(host).to_be_attached()
|
|
host.locator("[data-toggle]").click()
|
|
expect(host.locator("[data-menu]")).to_be_visible()
|
|
with page.expect_response(
|
|
lambda r: "/status" in r.url and r.request.method == "PATCH"
|
|
):
|
|
host.locator('[data-option][data-value="f"]').click()
|
|
expect(host.locator("[data-menu]")).to_be_hidden()
|
|
game.refresh_from_db()
|
|
assert game.status == "f"
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_session_device_selector_patches(authenticated_page: Page, live_server):
|
|
from games.models import Device, Game, Platform, Session
|
|
|
|
platform = Platform.objects.create(name="PC", icon="pc")
|
|
game = Game.objects.create(name="Test Game", platform=platform)
|
|
desktop = Device.objects.create(name="Desktop")
|
|
deck = Device.objects.create(name="Deck")
|
|
session = Session.objects.create(
|
|
game=game, device=desktop, timestamp_start="2025-01-01 00:00:00+00:00"
|
|
)
|
|
|
|
page = authenticated_page
|
|
page.goto(f"{live_server.url}{reverse('games:list_sessions')}")
|
|
|
|
host = page.locator("session-device-selector").first
|
|
expect(host).to_be_attached()
|
|
host.locator("[data-toggle]").click()
|
|
with page.expect_response(
|
|
lambda r: "/device" in r.url and r.request.method == "PATCH"
|
|
):
|
|
host.locator(f'[data-option][data-value="{deck.id}"]').click()
|
|
session.refresh_from_db()
|
|
assert session.device_id == deck.id
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_play_event_row_increments(authenticated_page: Page, live_server):
|
|
from games.models import Game, Platform
|
|
|
|
platform = Platform.objects.create(name="PC", icon="pc")
|
|
game = Game.objects.create(name="Test Game", platform=platform)
|
|
|
|
page = authenticated_page
|
|
page.goto(f"{live_server.url}{reverse('games:view_game', args=[game.id])}")
|
|
|
|
host = page.locator("play-event-row").first
|
|
expect(host).to_be_attached()
|
|
host.locator("[data-toggle]").click()
|
|
with page.expect_response(
|
|
lambda r: "playevent" in r.url.lower() and r.request.method == "POST"
|
|
):
|
|
host.locator("[data-add-play]").click()
|
|
expect(host.locator("[data-count]")).to_have_text("1")
|
|
assert game.playevents.count() == 1
|