feat: portfolio-led redesign with Shootings umbrella, original logo and motion layer
This commit is contained in:
@@ -262,6 +262,7 @@ class Content(unittest.TestCase):
|
||||
def test_all_requested_services_are_reachable(self):
|
||||
home = read(SITE / "index.html")
|
||||
for href in (
|
||||
"/shootings/",
|
||||
"/businessfotografie/",
|
||||
"/familien-und-paare/",
|
||||
"/minishootings/",
|
||||
@@ -313,6 +314,192 @@ class Content(unittest.TestCase):
|
||||
self.assertIn(f"Sitemap: {META['url']}/sitemap.xml", read(SITE / "robots.txt"))
|
||||
|
||||
|
||||
class Redesign(unittest.TestCase):
|
||||
"""The 2026 redesign: Shootings umbrella, real portfolio, logo, motion layer."""
|
||||
|
||||
def test_nav_uses_shootings_as_the_photo_umbrella(self):
|
||||
for file in HTML_FILES:
|
||||
text = read(file)
|
||||
with self.subTest(file=file.name):
|
||||
self.assertIn('href="/shootings/"', text)
|
||||
self.assertNotIn('href="/fotografie/', text)
|
||||
|
||||
def test_all_four_shootings_are_grouped_under_the_umbrella(self):
|
||||
shootings = read(SITE / "shootings" / "index.html")
|
||||
for href in (
|
||||
"/businessfotografie/",
|
||||
"/portrait-und-model/",
|
||||
"/familien-und-paare/",
|
||||
"/minishootings/",
|
||||
):
|
||||
with self.subTest(href=href):
|
||||
self.assertIn(f'href="{href}"', shootings)
|
||||
|
||||
def test_photo_and_video_are_separate_but_both_on_the_homepage(self):
|
||||
home = read(SITE / "index.html")
|
||||
self.assertEqual(home.count('<article class="pillar spot reveal">'), 2)
|
||||
self.assertIn('<a class="pillar__go" href="/shootings/">Shootings ansehen', home)
|
||||
self.assertIn('<a class="pillar__go" href="/video/">Videoprojekte ansehen', home)
|
||||
|
||||
def test_links_are_never_nested(self):
|
||||
"""Nested <a> is invalid and the parser splits the surrounding layout."""
|
||||
import html.parser
|
||||
|
||||
class AnchorGuard(html.parser.HTMLParser):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.depth = 0
|
||||
self.problems: list[str] = []
|
||||
self.file = ""
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
if tag == "a":
|
||||
self.depth += 1
|
||||
if self.depth > 1:
|
||||
self.problems.append(f"{self.file}: nested <a>")
|
||||
|
||||
def handle_endtag(self, tag):
|
||||
if tag == "a" and self.depth:
|
||||
self.depth -= 1
|
||||
|
||||
for file in HTML_FILES:
|
||||
guard = AnchorGuard()
|
||||
guard.file = file.name
|
||||
guard.feed(read(file))
|
||||
with self.subTest(file=file.name):
|
||||
self.assertEqual([], guard.problems)
|
||||
self.assertEqual(0, guard.depth, "unbalanced <a>")
|
||||
|
||||
def test_pillar_grid_has_exactly_two_children(self):
|
||||
"""Regression: the two pillars must stay two grid children."""
|
||||
home = read(SITE / "index.html")
|
||||
start = home.index('<div class="pillars"')
|
||||
end = home.index("</section>", start)
|
||||
self.assertEqual(2, home[start:end].count('<article class="pillar spot reveal">'))
|
||||
|
||||
def test_nginx_redirects_the_retired_fotografie_url(self):
|
||||
conf = read(ROOT / "nginx.conf")
|
||||
for line in (
|
||||
"location = /fotografie { return 301 /shootings/; }",
|
||||
"location = /fotografie/ { return 301 /shootings/; }",
|
||||
"location = /fotografie.html { return 301 /shootings/; }",
|
||||
):
|
||||
with self.subTest(line=line):
|
||||
self.assertIn(line, conf)
|
||||
# /fotografie/index.html is canonicalised to /fotografie/ by the
|
||||
# server-level index rule and reaches /shootings/ on the second hop.
|
||||
self.assertIn(r"if ($request_uri ~ ^/(.*/)?index\.html(\?.*)?$)", conf)
|
||||
|
||||
def test_retired_fotografie_build_output_is_gone(self):
|
||||
self.assertFalse((SITE / "fotografie").exists())
|
||||
self.assertFalse((SITE / "assets" / "img" / "og-fotografie.jpg").exists())
|
||||
for slug in (
|
||||
"people-forest",
|
||||
"people-veil",
|
||||
"people-walk",
|
||||
"hero-studio",
|
||||
"travel-blossom",
|
||||
"travel-japan",
|
||||
"travel-norway-coast",
|
||||
"travel-norway-lake",
|
||||
"travel-norway-pano",
|
||||
"travel-norway-valley",
|
||||
):
|
||||
for image in (SITE / "assets" / "img").glob(f"{slug}-*.webp"):
|
||||
self.fail(f"stale image built: {image.name}")
|
||||
|
||||
def test_sitemap_lists_the_new_route_only(self):
|
||||
sitemap = read(SITE / "sitemap.xml")
|
||||
self.assertIn("<loc>https://desfoto.de/shootings/</loc>", sitemap)
|
||||
self.assertNotIn("<loc>https://desfoto.de/fotografie/</loc>", sitemap)
|
||||
|
||||
def test_operator_logo_is_the_brand_mark(self):
|
||||
self.assertTrue((SITE / "assets" / "img" / "logo-160.png").is_file())
|
||||
for file in HTML_FILES:
|
||||
with self.subTest(file=file.name):
|
||||
self.assertIn(
|
||||
'<span class="brand__mark"><img src="/assets/img/logo-160.png"',
|
||||
read(file),
|
||||
)
|
||||
self.assertIn("data:image/png;base64,", read(SITE / "favicon.svg"))
|
||||
|
||||
def test_operator_logo_keeps_its_square_geometry(self):
|
||||
"""The derived icons are square resizes; a non-square logo would crop."""
|
||||
from PIL import Image
|
||||
|
||||
source = Image.open(ROOT / "assets-src" / "brand" / "logo.png")
|
||||
self.assertEqual(source.size[0], source.size[1], "the original logo must stay square")
|
||||
for name, size in (
|
||||
("logo-160.png", 160),
|
||||
("apple-touch-icon.png", 180),
|
||||
("favicon-32.png", 32),
|
||||
("favicon-16.png", 16),
|
||||
):
|
||||
with self.subTest(name=name):
|
||||
self.assertEqual((size, size), Image.open(SITE / "assets" / "img" / name).size)
|
||||
|
||||
def test_footer_links_the_operators_other_projects(self):
|
||||
for file in HTML_FILES:
|
||||
text = read(file)
|
||||
with self.subTest(file=file.name):
|
||||
for url in ("https://www.dennyschulz.de/", "https://dennyapp.de/"):
|
||||
self.assertIn(f'href="{url}"', text)
|
||||
self.assertIn('target="_blank" rel="noopener"', text)
|
||||
|
||||
def test_mobile_studio_is_described_where_photo_work_is_sold(self):
|
||||
needles = ("Mobiles Studio", "Autarke Blitzanlage", "autarker Blitzanlage")
|
||||
for rel in ("index.html", "shootings/index.html", "businessfotografie/index.html"):
|
||||
text = read(SITE / rel)
|
||||
for needle in needles:
|
||||
with self.subTest(file=rel, needle=needle):
|
||||
self.assertIn(needle, text)
|
||||
|
||||
def test_music_and_free_work_portfolio_is_present(self):
|
||||
projekte = read(SITE / "projekte" / "index.html")
|
||||
video = read(SITE / "video" / "index.html")
|
||||
self.assertIn("Musik", projekte)
|
||||
for slug in ("music-vocal-blue", "band-sagenbringer", "free-fjord", "free-fox"):
|
||||
with self.subTest(page="projekte", slug=slug):
|
||||
self.assertIn(f"/assets/img/{slug}-1200.webp", projekte)
|
||||
for slug in ("music-guitar-silhouette", "music-drums"):
|
||||
with self.subTest(page="video", slug=slug):
|
||||
self.assertIn(f"/assets/img/{slug}-1200.webp", video)
|
||||
|
||||
def test_invented_japan_content_is_absent(self):
|
||||
for file in HTML_FILES:
|
||||
text = read(file).lower()
|
||||
with self.subTest(file=file.name):
|
||||
for needle in ("japan", "kirschblüte", "travel-", "people-forest", "people-veil"):
|
||||
self.assertNotIn(needle, text)
|
||||
|
||||
def test_animation_layer_is_reduced_motion_safe(self):
|
||||
css = read(SITE / "assets" / "site.css")
|
||||
self.assertIn("@media (prefers-reduced-motion:reduce)", css)
|
||||
motion = css[css.index("@media (prefers-reduced-motion:reduce)") :]
|
||||
for needle in (
|
||||
".reveal,.reveal--mask{opacity:1",
|
||||
"animation:none",
|
||||
".scroll-cue{display:none}",
|
||||
):
|
||||
with self.subTest(needle=needle):
|
||||
self.assertIn(needle, motion)
|
||||
# The reduced-motion block must win over the motion definitions.
|
||||
self.assertGreater(css.rindex("@media (prefers-reduced-motion:reduce)"), css.rindex("animation:hero-zoom"))
|
||||
|
||||
def test_every_page_has_a_noscript_reveal_fallback(self):
|
||||
for file in HTML_FILES:
|
||||
text = read(file)
|
||||
with self.subTest(file=file.name):
|
||||
self.assertIn("<noscript><style>", text)
|
||||
self.assertIn(".reveal,.reveal--mask{opacity:1", text)
|
||||
|
||||
def test_scroll_animations_are_progressive_enhancement(self):
|
||||
js = read(SITE / "assets" / "site.js")
|
||||
for needle in ("prefers-reduced-motion", "IntersectionObserver", "data-stagger"):
|
||||
with self.subTest(needle=needle):
|
||||
self.assertIn(needle, js)
|
||||
|
||||
|
||||
class Release(unittest.TestCase):
|
||||
def test_deploy_removes_only_targeted_desfoto_labels(self):
|
||||
deploy = read(ROOT / ".ocauto" / "deploy")
|
||||
|
||||
Reference in New Issue
Block a user