feat: phase 1 public site skeleton — layout, routes, CSS, logo pipeline
Ship base Jinja layout (header/nav/main/footer with skip link and aria-current), mobile-first single-file CSS using the ROADMAP palette tokens, and four public routes: /, /about, /contact, /shop. Blog index renders via a stable PostService.list_published() stub returning [] — Phase 2 only swaps the body. About is static placeholder copy, /contact ships an inert form plus a mailto: link driven by ADMIN_CONTACT_EMAIL, /shop shows a "Coming soon" card. Adds a Pillow-based scripts/generate_static_assets.py producing resized logo PNG + WebP, multi-size favicon.ico, and a 180x180 apple-touch-icon on a cream background. Outputs committed for a reproducible build. Also ship docs/MANUAL_TESTING.md with per-route / responsive / a11y / static- asset checklists, and mark Phase 1 complete in docs/ROADMAP.md.
This commit is contained in:
103
tests/test_public_routes.py
Normal file
103
tests/test_public_routes.py
Normal file
@@ -0,0 +1,103 @@
|
||||
"""Smoke tests for the public-site skeleton routes.
|
||||
|
||||
These tests focus on contract rather than styling:
|
||||
|
||||
- every public route returns 200 with an HTML content-type
|
||||
- each page contains a page-specific substring (proves the template
|
||||
actually rendered, not just that the route exists)
|
||||
- the homepage renders the empty-state copy when PostService returns []
|
||||
- the shared layout emits the logo image path so nav/logo aren't broken
|
||||
by a future refactor
|
||||
|
||||
No mocks of the DB (there is no DB in Phase 1). The PostService stub
|
||||
already returns an empty list, which is exactly what we want to assert
|
||||
against.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.main import app
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def client() -> TestClient:
|
||||
"""Return a module-scoped FastAPI TestClient.
|
||||
|
||||
TestClient uses the module-level `app` built by `create_app()` at
|
||||
import time, i.e. the exact same app uvicorn runs in production.
|
||||
"""
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path,expected_substring",
|
||||
[
|
||||
("/", "Chicken Babies"),
|
||||
("/about", "About the farm"),
|
||||
("/contact", "Get in touch"),
|
||||
("/shop", "Coming soon"),
|
||||
],
|
||||
)
|
||||
def test_public_route_renders_html(
|
||||
client: TestClient,
|
||||
path: str,
|
||||
expected_substring: str,
|
||||
) -> None:
|
||||
"""Every public page returns 200 HTML containing a page-specific string.
|
||||
|
||||
The substring is intentionally a headline the template owns so the
|
||||
test fails loudly if the wrong template is accidentally wired up.
|
||||
"""
|
||||
response = client.get(path)
|
||||
|
||||
assert response.status_code == 200, (
|
||||
f"{path} returned {response.status_code}: {response.text[:200]}"
|
||||
)
|
||||
|
||||
content_type = response.headers.get("content-type", "")
|
||||
assert content_type.startswith("text/html"), (
|
||||
f"{path} returned unexpected content-type: {content_type!r}"
|
||||
)
|
||||
|
||||
assert expected_substring in response.text, (
|
||||
f"{path} body missing expected substring {expected_substring!r}"
|
||||
)
|
||||
|
||||
|
||||
def test_home_shows_empty_state_when_no_posts(client: TestClient) -> None:
|
||||
"""With the Phase 1 stub service, the home page shows 'No posts yet'.
|
||||
|
||||
This is the canonical empty-state marker; Phase 2 seeds a welcome
|
||||
post so this test will need to be updated when the DB lands.
|
||||
"""
|
||||
response = client.get("/")
|
||||
assert response.status_code == 200
|
||||
assert "No posts yet" in response.text
|
||||
|
||||
|
||||
def test_layout_includes_logo_image(client: TestClient) -> None:
|
||||
"""Shared layout references the generated logo asset paths.
|
||||
|
||||
We check for the stem (``/static/img/logo.``) rather than a specific
|
||||
extension so both the <source srcset="...webp"> and <img src="...png">
|
||||
markup are covered by a single assertion.
|
||||
"""
|
||||
response = client.get("/")
|
||||
assert response.status_code == 200
|
||||
assert "/static/img/logo." in response.text
|
||||
|
||||
|
||||
def test_nav_marks_active_page(client: TestClient) -> None:
|
||||
"""The About page renders ``aria-current=\"page\"`` on its nav link.
|
||||
|
||||
Exercises the shared layout's active-nav logic end-to-end without
|
||||
inspecting internals.
|
||||
"""
|
||||
response = client.get("/about")
|
||||
assert response.status_code == 200
|
||||
# The pair should appear exactly once per active link; we only need
|
||||
# to prove it's present at all.
|
||||
assert 'aria-current="page"' in response.text
|
||||
Reference in New Issue
Block a user