post composer: restore validation-error reveal broken by the .composer-row wrap; CI FT cleanup — TDD
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful

The OK-button composer redesign wrapped the line input in .composer-row, so the .form-control.is-invalid input is no longer a general SIBLING of its .invalid-feedback — the `&.is-invalid ~ .invalid-feedback` reveal (\_base.scss) silently stopped matching, so post-line validation errors rendered in the DOM but stayed display:none (invisible to users). Reveal via `.composer-row:has(.form-control.is-invalid) ~ .invalid-feedback`. Greens test_cannot_add_duplicate_lines + test_error_messages_are_cleared_on_input (both were catching this real regression, not flaky).

Harden WalletShopFreeDeckTest: the .tt-micro is briefly detached mid-HTMX-swap, so get_attribute('innerHTML') returns None and a bare assertIn raises TypeError — which wait_for does NOT retry. Coalesce to '' so it polls until the swap settles (explains the local-pass / CI-fail).

Delete test_core_styling.test_layout_and_styling: a Percival-era assertion that the post input is horizontally CENTRED in its section. The responsive .composer-row (input + OK btn) + the orientation-aware right-margin clamp intentionally removed that invariant (the input now lands in different spots per viewport). Zero behavioural coverage lost — the composer is covered by LineValidationTest + PostComposerOkButtonTest.

Skip GameViewsCarouselTest (red planning contract for the unbuilt Game-views carousel — see project-room-game-views-carousel).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-06-02 02:54:31 -04:00
parent 39a42a33a3
commit 62743aabd0
4 changed files with 19 additions and 38 deletions

View File

@@ -1,37 +0,0 @@
from .base import FunctionalTest
from .post_page import PostPage
class LayoutAndStylingTest(FunctionalTest):
def test_layout_and_styling(self):
self.create_pre_authenticated_session("disco@test.io")
self.browser.get(self.live_server_url + '/billboard/')
post_page = PostPage(self)
self.browser.set_window_size(1024, 768)
def section_center(el):
return self.browser.execute_script("""
var el = arguments[0];
var ancestor = el.parentElement;
while (ancestor && ancestor.tagName !== 'SECTION'
&& !ancestor.classList.contains('container')) {
ancestor = ancestor.parentElement;
}
var a = ancestor || document.body;
var s = a.getBoundingClientRect();
var cs = window.getComputedStyle(a);
var pl = parseFloat(cs.paddingLeft);
var pr = parseFloat(cs.paddingRight);
var r = el.getBoundingClientRect();
return [r.x + r.width / 2, s.x + pl + (s.width - pl - pr) / 2];
""", el)
inputbox = post_page.get_line_input_box()
input_c, section_c = section_center(inputbox)
self.assertAlmostEqual(input_c, section_c, delta=10)
post_page.add_post_line("testing")
inputbox = post_page.get_line_input_box()
input_c, section_c = section_center(inputbox)
self.assertAlmostEqual(input_c, section_c, delta=10)

View File

@@ -491,11 +491,15 @@ class WalletShopFreeDeckTest(FunctionalTest):
# + may not be hover-visible under Selenium's strict-target check). # + may not be hover-visible under Selenium's strict-target check).
self.browser.execute_script("arguments[0].click();", free_btn) self.browser.execute_script("arguments[0].click();", free_btn)
# 3. The tile now shows 'Already owned' instead of the FREE ITEM btn. # 3. The tile now shows 'Already owned' instead of the FREE ITEM btn.
# `or ""` — the .tt-micro is briefly detached mid-HTMX-swap, when
# get_attribute("innerHTML") returns None; a bare assertIn then raises
# TypeError (which wait_for does NOT retry). Coalesce to "" so it polls
# until the swap settles.
self.wait_for(lambda: self.assertIn( self.wait_for(lambda: self.assertIn(
"Already owned", "Already owned",
self.browser.find_element( self.browser.find_element(
By.CSS_SELECTOR, "#id_shop_tarot-rider-waite-smith .tt-micro" By.CSS_SELECTOR, "#id_shop_tarot-rider-waite-smith .tt-micro"
).get_attribute("innerHTML"), ).get_attribute("innerHTML") or "",
)) ))
self.assertEqual( self.assertEqual(
self.browser.find_elements( self.browser.find_elements(

View File

@@ -24,6 +24,8 @@ These tests fail until the feature lands — they ARE the contract for the build
FT bucket: game_room. IT/UT coverage (view context, room↔Post link, aggregate FT bucket: game_room. IT/UT coverage (view context, room↔Post link, aggregate
merge) is written beneath these as the build proceeds. merge) is written beneath these as the build proceeds.
""" """
from unittest import skip
from selenium.webdriver.common.by import By from selenium.webdriver.common.by import By
from .base import FunctionalTest from .base import FunctionalTest
@@ -35,6 +37,8 @@ from apps.lyric.models import User
VIEW_ORDER = ["atlas", "scroll", "post", "chat", "pulse"] VIEW_ORDER = ["atlas", "scroll", "post", "chat", "pulse"]
@skip("Game-views carousel — RED contract; feature unbuilt. "
"Remove the skip as each view lands. See project-room-game-views-carousel.")
class GameViewsCarouselTest(FunctionalTest): class GameViewsCarouselTest(FunctionalTest):
def setUp(self): def setUp(self):
super().setUp() super().setUp()

View File

@@ -184,6 +184,16 @@ body {
margin-top: 0.25rem; margin-top: 0.25rem;
} }
// The post composer wraps its input + OK btn in `.composer-row`, so the
// `.form-control.is-invalid` input is no longer a general SIBLING of its
// `.invalid-feedback` (they live in different parents) — the
// `&.is-invalid ~ .invalid-feedback` reveal above silently stops
// matching, so the error renders in the DOM but stays display:none and
// the user never sees post-line validation. Reveal via :has() on the row.
.composer-row:has(.form-control.is-invalid) ~ .invalid-feedback {
display: block;
}
.alert { .alert {
padding: 0.75rem 1rem; padding: 0.75rem 1rem;
margin: 0.75rem; margin: 0.75rem;