FUCKER
Some checks failed
DevPlace CI / test (push) Failing after 1h3m34s

This commit is contained in:
retoor 2026-08-09 11:39:53 +02:00
parent c0742994cd
commit 1a5fc9428a
14 changed files with 188 additions and 17 deletions

View File

@ -37,6 +37,6 @@ One global rule at the end of `base.css` collapses every animation/transition to
- Flexbox + CSS Grid with `gap` only; floats for layout are forbidden. - Flexbox + CSS Grid with `gap` only; floats for layout are forbidden.
- Approved page layouts and the shared shell are documented in `/docs/styles-layout.html`; a page supplies exactly one layout container inside `.page`. - Approved page layouts and the shared shell are documented in `/docs/styles-layout.html`; a page supplies exactly one layout container inside `.page`.
- Every fluid grid/flex column sets `min-width: 0`. - Every fluid grid/flex column sets `min-width: 0`, and every fluid grid track that holds content is written `minmax(0, 1fr)`, never a bare `1fr`. A `1fr` track's automatic minimum is the item's min-content size, so one unwrappable line inside a rendered code block (`.rendered-content pre`, `white-space: pre`) widens the track and blows the whole page open sideways. This is why a detail page (a `max-width` block, definite width, the `pre` scrolls inside it) survives content that destroys a listing grid. The rule covers a column that is fixed-width at desktop but becomes the single fluid column at a breakpoint (`.profile-sidebar`), and it is regression-tested by `assert_no_horizontal_overflow` in `tests/conftest.py`.
- `!important` is allowed only for: the `.hidden`/`[hidden]` display utilities, the global reduced-motion rule, and the devii-avatar third-party-beating override. Anything else is a specificity problem to be fixed structurally. - `!important` is allowed only for: the `.hidden`/`[hidden]` display utilities, the global reduced-motion rule, and the devii-avatar third-party-beating override. Anything else is a specificity problem to be fixed structurally.
- Page-specific CSS lives in its own `static/css/*.css` loaded via `{% block extra_head %}`, never an inline `<style>` block; shared component styles live once (`components.css`, `feed.css` vote buttons) and are never redefined per page. - Page-specific CSS lives in its own `static/css/*.css` loaded via `{% block extra_head %}`, never an inline `<style>` block; shared component styles live once (`components.css`, `feed.css` vote buttons) and are never redefined per page.

View File

@ -43,7 +43,7 @@
@media (max-width: 768px) { @media (max-width: 768px) {
.awards-grid { .awards-grid {
grid-template-columns: 1fr; grid-template-columns: minmax(0, 1fr);
} }
} }

View File

@ -7,6 +7,9 @@
align-items: start; align-items: start;
} }
.feed-main {
min-width: 0;
}
.feed-nav { .feed-nav {
display: flex; display: flex;

View File

@ -17,6 +17,10 @@
align-items: start; align-items: start;
} }
.gists-main {
min-width: 0;
}
.gists-header { .gists-header {
display: flex; display: flex;
align-items: center; align-items: center;
@ -36,7 +40,7 @@
.gists-grid { .gists-grid {
display: grid; display: grid;
grid-template-columns: 1fr; grid-template-columns: minmax(0, 1fr);
gap: 1rem; gap: 1rem;
} }

View File

@ -141,6 +141,10 @@
align-items: start; align-items: start;
} }
.dashboard-feed {
min-width: 0;
}
.dashboard-feed-header { .dashboard-feed-header {
display: flex; display: flex;
align-items: center; align-items: center;
@ -167,7 +171,7 @@
.dashboard-posts-grid { .dashboard-posts-grid {
display: grid; display: grid;
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.875rem; gap: 0.875rem;
} }
@ -373,7 +377,7 @@
align-items: flex-start; align-items: flex-start;
} }
.dashboard-posts-grid { .dashboard-posts-grid {
grid-template-columns: 1fr; grid-template-columns: minmax(0, 1fr);
} }
} }
@ -713,7 +717,7 @@
.landing-posts-grid { .landing-posts-grid {
display: grid; display: grid;
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1rem; gap: 1rem;
} }
@ -850,7 +854,7 @@
padding: 0 1rem; padding: 0 1rem;
} }
.landing-posts-grid { .landing-posts-grid {
grid-template-columns: 1fr; grid-template-columns: minmax(0, 1fr);
} }
} }

View File

@ -24,7 +24,7 @@
.news-grid { .news-grid {
display: grid; display: grid;
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1.25rem; gap: 1.25rem;
} }
@ -283,7 +283,7 @@
@media (max-width: 768px) { @media (max-width: 768px) {
.news-grid { .news-grid {
grid-template-columns: 1fr; grid-template-columns: minmax(0, 1fr);
} }
.news-header h1 { .news-header h1 {

View File

@ -10,6 +10,7 @@
.profile-sidebar { .profile-sidebar {
position: sticky; position: sticky;
top: calc(var(--nav-height) + 1rem); top: calc(var(--nav-height) + 1rem);
min-width: 0;
} }
.profile-card { .profile-card {

View File

@ -7,6 +7,10 @@
align-items: start; align-items: start;
} }
.projects-main {
min-width: 0;
}
.projects-tabs { .projects-tabs {
display: flex; display: flex;
gap: 0.25rem; gap: 0.25rem;
@ -177,13 +181,13 @@
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.projects-grid { .projects-grid {
grid-template-columns: 1fr; grid-template-columns: minmax(0, 1fr);
} }
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.projects-grid { .projects-grid {
grid-template-columns: 1fr; grid-template-columns: minmax(0, 1fr);
} }
.projects-header { .projects-header {

View File

@ -85,7 +85,7 @@ Content sits on cards built from tokens: `--bg-card`, a `--border` hairline, `--
- [ ] `{% extends "base.html" %}`; CSS via `extra_head`, JS via `extra_js`. - [ ] `{% extends "base.html" %}`; CSS via `extra_head`, JS via `extra_js`.
- [ ] Two or more breadcrumbs passed in the SEO context (content clears the nav). - [ ] Two or more breadcrumbs passed in the SEO context (content clears the nav).
- [ ] Exactly one layout container inside `.page`; no second `max-width` wrapper. - [ ] Exactly one layout container inside `.page`; no second `max-width` wrapper.
- [ ] Multi-column = grid with `<aside>` rails, fluid column has `min-width: 0`, collapses at `1024px`. - [ ] Multi-column = grid with `<aside>` rails, fluid column has `min-width: 0`, card-grid tracks are `minmax(0, 1fr)`, collapses at `1024px`.
- [ ] One `<h1>` (use `.sr-only` if visually redundant). - [ ] One `<h1>` (use `.sr-only` if visually redundant).
- [ ] No custom header or footer; both come from `base.html`. - [ ] No custom header or footer; both come from `base.html`.
- [ ] Only tokens for colour, spacing, radius, shadow, and z-index; no `var()` fallback values. - [ ] Only tokens for colour, spacing, radius, shadow, and z-index; no `var()` fallback values.

View File

@ -111,8 +111,8 @@ A new stacked element picks the token matching its role; if none fits, add a tok
## Acceptable and not ## Acceptable and not
- **Acceptable:** one layout container inside `.page`; CSS Grid for columns; `<aside>` for side columns; `min-width: 0` on the fluid column; sticky rails; cards built from tokens. - **Acceptable:** one layout container inside `.page`; CSS Grid for columns; `<aside>` for side columns; `min-width: 0` on the fluid column; `minmax(0, 1fr)` on card-grid tracks; sticky rails; cards built from tokens.
- **Not acceptable:** a second `max-width`/centred wrapper inside `.page`; absolute positioning to fake columns; fixed pixel heights on content; a side column that does not collapse on small screens; a main column without `min-width: 0` (it will overflow on long code lines). - **Not acceptable:** a second `max-width`/centred wrapper inside `.page`; absolute positioning to fake columns; fixed pixel heights on content; a side column that does not collapse on small screens; a main column without `min-width: 0`, or a card grid on a bare `1fr` track (both overflow on long code lines - the track's automatic minimum is the item's min-content width, and a code block never wraps).
{% endraw %} {% endraw %}
</div> </div>

View File

@ -290,6 +290,35 @@ def assert_share_copies(page, expected_fragment):
) )
def assert_no_horizontal_overflow(page, layout):
report = page.evaluate(
"""(selector) => {
const root = document.documentElement;
const container = document.querySelector(selector);
const widest = [];
document.querySelectorAll("body *").forEach((el) => {
if (el.getBoundingClientRect().right <= root.clientWidth + 1) return;
widest.push(
el.tagName.toLowerCase() + "." + (el.className || "").toString().trim()
);
});
return {
page: root.scrollWidth - root.clientWidth,
layout: container ? container.scrollWidth - container.clientWidth : null,
widest: widest.slice(0, 5),
};
}""",
layout,
)
assert report["layout"] is not None, f"{layout} not found"
assert report["page"] <= 1, (
f"page scrolls horizontally by {report['page']}px: {report['widest']}"
)
assert report["layout"] <= 1, (
f"{layout} overflows its own width by {report['layout']}px: {report['widest']}"
)
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def seeded_db(app_server): def seeded_db(app_server):
"""Create test users once at session level using requests directly.""" """Create test users once at session level using requests directly."""

View File

@ -37,7 +37,11 @@ def _goto_feed_until(page, predicate, timeout=5.0):
import re import re
from uuid import uuid4 from uuid import uuid4
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from tests.conftest import BASE_URL, assert_share_copies from tests.conftest import (
BASE_URL,
assert_no_horizontal_overflow,
assert_share_copies,
)
from devplacepy.database import get_table from devplacepy.database import get_table
from devplacepy.utils import make_combined_slug from devplacepy.utils import make_combined_slug
def _seed_posts(count): def _seed_posts(count):
@ -965,3 +969,58 @@ def test_feed_scroll_not_restored_on_fresh_visit(alice):
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded") page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
page.wait_for_timeout(600) page.wait_for_timeout(600)
assert page.evaluate("window.scrollY") < 60 assert page.evaluate("window.scrollY") < 60
def _seed_post_with_long_code_line(topic="devlog"):
owner = str(uuid4())
get_table("users").insert(
{
"uid": owner,
"username": f"code_{owner[:8]}",
"email": f"{owner[:8]}@code.seed",
"password_hash": "x",
"role": "Member",
"is_active": True,
"created_at": datetime.now(timezone.utc).isoformat(),
}
)
uid = str(uuid4())
marker = f"longcode-{uid[:8]}"
line = "unbreakable_identifier_" * 10
get_table("posts").insert(
{
"deleted_at": None,
"deleted_by": None,
"uid": uid,
"user_uid": owner,
"slug": make_combined_slug(marker, uid),
"title": marker,
"content": f"```python\n{line}\n```",
"topic": topic,
"project_uid": None,
"image": None,
"stars": 0,
"created_at": datetime.now(timezone.utc).isoformat(),
}
)
return marker
def test_feed_long_code_line_does_not_widen_layout(alice):
page, user = alice
marker = _seed_post_with_long_code_line()
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
card = page.locator(".post-card").filter(has_text=marker).first
card.wait_for(state="visible")
assert card.locator("pre").count() == 1
assert_no_horizontal_overflow(page, ".feed-layout")
def test_dashboard_long_code_line_does_not_widen_layout(alice):
page, user = alice
marker = _seed_post_with_long_code_line()
page.goto(f"{BASE_URL}/", wait_until="domcontentloaded")
card = page.locator(".dashboard-post-card").filter(has_text=marker).first
card.wait_for(state="visible")
assert card.locator("pre").count() == 1
assert_no_horizontal_overflow(page, ".dashboard-main")

View File

@ -6,7 +6,11 @@ import requests
from uuid import uuid4 from uuid import uuid4
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from playwright.sync_api import expect from playwright.sync_api import expect
from tests.conftest import BASE_URL, assert_share_copies from tests.conftest import (
BASE_URL,
assert_no_horizontal_overflow,
assert_share_copies,
)
from devplacepy.database import get_table from devplacepy.database import get_table
from devplacepy.utils import make_combined_slug from devplacepy.utils import make_combined_slug
DPBOT_SIZE = 112147 DPBOT_SIZE = 112147
@ -473,3 +477,46 @@ def test_gists_list_preserves_comment_hierarchy(page, app_server):
expect(card.locator(".post-card-comments")).not_to_contain_text(f"{marker}-excluded") expect(card.locator(".post-card-comments")).not_to_contain_text(f"{marker}-excluded")
nested = card.locator(".post-card-comments .comment-replies .comment-text") nested = card.locator(".post-card-comments .comment-replies .comment-text")
expect(nested).to_contain_text(f"{marker}-reply") expect(nested).to_contain_text(f"{marker}-reply")
def _seed_gist_with_long_code_line():
owner = str(uuid4())
get_table("users").insert(
{
"uid": owner,
"username": f"gcode_{owner[:8]}",
"email": f"{owner[:8]}@gcode.seed",
"password_hash": "x",
"role": "Member",
"is_active": True,
"created_at": datetime.now(timezone.utc).isoformat(),
}
)
uid = str(uuid4())
marker = f"gistcode-{uid[:8]}"
line = "unbreakable_identifier_" * 8
get_table("gists").insert(
{
"deleted_at": None,
"deleted_by": None,
"uid": uid,
"user_uid": owner,
"slug": make_combined_slug(marker, uid),
"title": marker,
"description": f"```python\n{line}\n```",
"source_code": "print('x')",
"language": "python",
"stars": 0,
"created_at": datetime.now(timezone.utc).isoformat(),
}
)
return marker
def test_gists_long_code_line_does_not_widen_layout(page, app_server):
marker = _seed_gist_with_long_code_line()
page.goto(f"{BASE_URL}/gists", wait_until="domcontentloaded")
card = page.locator(".gist-card").filter(has_text=marker).first
card.wait_for(state="visible")
assert card.locator("pre").count() == 1
assert_no_horizontal_overflow(page, ".gists-layout")

View File

@ -2,7 +2,7 @@
from playwright.sync_api import expect from playwright.sync_api import expect
from tests.conftest import BASE_URL from tests.conftest import BASE_URL, assert_no_horizontal_overflow
CONSENT = "ai_third_party" CONSENT = "ai_third_party"
@ -65,3 +65,23 @@ def test_a_stranger_never_reaches_the_privacy_panel(bob):
) )
expect(page.locator(".privacy-panel")).to_have_count(0) expect(page.locator(".privacy-panel")).to_have_count(0)
expect(page.locator("form[action='/profile/alice_test/consent']")).to_have_count(0) expect(page.locator("form[action='/profile/alice_test/consent']")).to_have_count(0)
def test_profile_mobile_long_code_bio_does_not_widen_layout(mobile_page):
page, user = mobile_page
bio = "```python\n" + "unbreakable_identifier_" * 8 + "\n```"
page.request.post(
f"{BASE_URL}/profile/update",
form={"bio": bio, "location": "", "git_link": "", "website": ""},
)
try:
page.goto(
f"{BASE_URL}/profile/{user['username']}", wait_until="domcontentloaded"
)
page.locator(".profile-sidebar pre").first.wait_for(state="visible")
assert_no_horizontal_overflow(page, ".profile-layout")
finally:
page.request.post(
f"{BASE_URL}/profile/update",
form={"bio": "", "location": "", "git_link": "", "website": ""},
)