Files
landingpage-haus-schleusingen/tests/Controllers/LocaleSwitcherTest.php
Hermes 08235b0faf refactor(locale-switcher): single flag-sized dropdown, drop 4-inline-flag UI
The nav previously showed 4 inline flag buttons (DE/EN/UK/RU) on desktop
and a details-based dropdown on mobile. Martin asked for one dropdown with
a trigger the size of a single flag, and the 4 inline flags to go away.

- LocaleSwitcher: render a single <details class='locale-switcher'>
  everywhere; trigger is one flag + tiny caret; menu lists all 4 with labels.
- Drop the 4-inline <ul> and the locale-switcher-mobile duplicate.
- CSS: replace both blocks with one compact dropdown (flag-sized trigger,
  44px touch target via padding, scrolled/transparent-nav variants).
- Tests: assert 4 menu options, 5 flag SVGs, single <details> dropdown,
  active locale is a <span aria-current>, others are <a> with hreflang.
- 141/141 PHPUnit green.
2026-06-04 18:24:36 +00:00

153 lines
6.0 KiB
PHP

<?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', '#FFCC00'],
'EN UnionJack' => ['en', '#C8102E'],
'UK Ukraine' => ['uk', '#FFD500'],
'RU Russia' => ['ru', '#D52B1E'],
];
}
#[Test]
public function flagSvgReturnsValidSvgForEverySupportedLocale(): void
{
foreach (Locale::SUPPORTED as $code) {
$svg = LocaleSwitcher::flagSvg($code);
self::assertStringStartsWith('<svg', $svg);
self::assertStringContainsString('viewBox="0 0 24 16"', $svg);
self::assertStringContainsString('aria-hidden="true"', $svg);
self::assertStringContainsString('focusable="false"', $svg);
self::assertStringContainsString('class="flag"', $svg);
self::assertStringEndsWith('</svg>', $svg);
}
}
#[Test]
public function flagSvgHasFallbackForUnknownLocale(): void
{
$svg = LocaleSwitcher::flagSvg('xx');
self::assertStringStartsWith('<svg', $svg);
self::assertStringContainsString('class="flag"', $svg);
self::assertStringEndsWith('</svg>', $svg);
}
#[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 <svg class="flag"> in the document is the trigger
self::assertStringContainsString('<svg class="flag" viewBox="0 0 24 16"', $html);
// The first flag SVG must contain the German colours
$deFlag = LocaleSwitcher::flagSvg('de');
$pos = strpos($html, $deFlag);
self::assertNotFalse($pos, 'expected German flag SVG in the trigger (first <svg> in document)');
}
}