Files
landingpage-haus-schleusingen/tests/Controllers/LocaleSwitcherTest.php
Hermes acaea97415 fix(locale-switcher): make flag the visual anchor (32x24, no border, no lazy load)
Martin feedback round 3: dropdown still looked 'fuerchterlich' even
with the official flag-icons. Root cause: 14px vertical padding
around an 18px-tall flag meant the flag occupied only 39% of the
trigger height and was dwarfed by whitespace. Plus a 1px black
box-shadow border made flags look 'boxy', and loading='lazy' caused
empty boxes on the four menu flags the moment the <details> opened.

Changes:
- Flag size 24x18 -> 32x24 (+78% area, ~4:3 matches flag-icons)
- Trigger padding 14px 8px -> 6px (flag now 73% of trigger width,
  55% of trigger height, was 46%/39%)
- Drop the artificial 1px black box-shadow outline on flags
- Drop border-radius on flags (real flag-icons look better as
  crisp rectangles)
- Drop object-fit: cover (no longer needed for SVG)
- Drop loading='lazy' and decoding='async' (4 small SVGs, must
  be ready the moment <details> opens, not flash empty boxes)
- min-height: 44px restored on trigger for WCAG 2.5.5 touch target
- Menu border-radius 8 -> 10px, padding tightened, font-size 0.85
  -> 0.9rem for label legibility
- Two-layer box-shadow on menu for subtle elevation
2026-06-05 17:05:01 +00:00

154 lines
6.1 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
declare(strict_types=1);
namespace Tests\Controllers;
use App\Controllers\LocaleSwitcher;
use App\Core\Locale;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
/**
* Renders the language switcher widget and checks that:
* - exactly one <details class="locale-switcher"> dropdown,
* - 4 menu items, one per supported locale,
* - the active locale is marked aria-current="true" and is a <span>,
* - inactive locales are <a> links to /locale?set=...&return=...,
* - the trigger and every menu item contain a flag SVG,
* - the rendered label is in the current locale's language.
*/
final class LocaleSwitcherTest extends TestCase
{
#[Test]
public function rendersSingleDropdownForAllSupportedLocales(): void
{
$html = (new LocaleSwitcher('en', '/'))->render();
// exactly one <details class="locale-switcher"> (no -mobile suffix, no desktop <ul>)
self::assertStringContainsString('<details class="locale-switcher">', $html);
self::assertStringNotContainsString('locale-switcher-mobile', $html);
self::assertStringNotContainsString('<ul class="locale-switcher"', $html);
self::assertStringNotContainsString('locale-switcher__item', $html);
// the menu lists all 4 supported locales
self::assertSame(4, substr_count($html, 'class="locale-switcher__option'), 'expected 4 menu options');
// The 3 inactive locales render as <a hreflang="..">. The active
// locale renders as <span lang=".."> (no hreflang). Together all
// 4 must be present in either form.
foreach (Locale::SUPPORTED as $code) {
self::assertTrue(
str_contains($html, 'hreflang="' . $code . '"') || str_contains($html, 'lang="' . $code . '"'),
"locale '$code' is missing from switcher",
);
}
// 1 flag in trigger + 4 flags in menu = 5 total
self::assertSame(5, substr_count($html, 'class="flag"'), 'expected 5 flag SVGs (1 trigger + 4 menu)');
}
#[Test]
public function marksCurrentLocaleWithAriaCurrentAndSpan(): void
{
$html = (new LocaleSwitcher('uk', '/'))->render();
self::assertStringContainsString('is-current', $html);
self::assertStringContainsString('aria-current="true"', $html);
self::assertStringContainsString('lang="uk"', $html);
// active option must be a <span>, not an <a>
self::assertMatchesRegularExpression(
'/<span class="locale-switcher__option is-current"[^>]*aria-current="true"[^>]*lang="uk"/',
$html,
);
}
#[Test]
public function inactiveLocalesAreLinksToLocaleController(): void
{
$html = (new LocaleSwitcher('de', '/foo/bar'))->render();
self::assertStringContainsString('href="/locale?set=en&amp;return=%2Ffoo%2Fbar"', $html);
self::assertStringContainsString('href="/locale?set=uk&amp;return=%2Ffoo%2Fbar"', $html);
self::assertStringContainsString('href="/locale?set=ru&amp;return=%2Ffoo%2Fbar"', $html);
}
#[Test]
public function stripsQueryAndFragmentFromReturnPath(): void
{
$html = (new LocaleSwitcher('de', '/?lang=uk#kontakt'))->render();
// sanitisePath keeps only the path part
self::assertStringContainsString('return=%2F', $html);
self::assertStringNotContainsString('return=%2F%3Flang', $html);
self::assertStringNotContainsString('return=%2F%23kontakt', $html);
}
#[Test]
public function rejectsPathsThatDoNotStartWithSlash(): void
{
$html = (new LocaleSwitcher('de', 'https://evil.example/'))->render();
// sanitisePath falls back to '/'
self::assertStringContainsString('return=%2F', $html);
self::assertStringNotContainsString('evil.example', $html);
}
/**
* @return array<string, array{string, string}>
*/
public static function flagDataProvider(): array
{
return [
'DE Germany' => ['de', 'de.svg'],
'EN en-GB' => ['en', 'gb.svg'],
'UK Ukraine' => ['uk', 'ua.svg'],
'RU Russia' => ['ru', 'ru.svg'],
];
}
#[Test]
public function flagImgReturnsValidImgForEverySupportedLocale(): void
{
foreach (Locale::SUPPORTED as $code) {
$img = LocaleSwitcher::flagImg($code);
self::assertStringStartsWith('<img', $img);
self::assertStringContainsString('class="flag"', $img);
self::assertStringContainsString('width="32" height="24"', $img);
self::assertStringContainsString('alt=""', $img);
self::assertStringEndsWith('>', $img);
}
}
#[Test]
public function flagImgHasFallbackForUnknownLocale(): void
{
$img = LocaleSwitcher::flagImg('xx');
self::assertStringStartsWith('<img', $img);
self::assertStringContainsString('class="flag"', $img);
// 1×1 transparent gif keeps the layout stable even when the
// locale code is not one of our four.
self::assertStringContainsString('data:image/gif', $img);
}
#[Test]
public function ariaLabelUsesCurrentLocaleName(): void
{
$htmlDe = (new LocaleSwitcher('de', '/'))->render();
$htmlEn = (new LocaleSwitcher('en', '/'))->render();
self::assertStringContainsString('aria-label="Sprache wählen"', $htmlDe);
self::assertStringContainsString('aria-label="Choose language"', $htmlEn);
}
#[Test]
public function triggerContainsCurrentLocaleFlag(): void
{
// The closed dropdown shows the current locale's flag in the trigger
$html = (new LocaleSwitcher('de', '/'))->render();
// The first <img class="flag"> in the document is the trigger and it
// must point at the German flag asset under /img/flags/.
$deFlag = LocaleSwitcher::flagImg('de');
$pos = strpos($html, $deFlag);
self::assertNotFalse($pos, 'expected German flag <img> in the trigger (first <img class="flag"> in document)');
self::assertStringContainsString('src="/img/flags/de.svg"', $deFlag);
}
}