<?php
declare(strict_types=1);
require __DIR__ . '/../src/ThemePreferenceDetector.php';
use ThemeSync\ThemePreferenceDetector;
$failures = 0;
$passed = 0;
/**
* @param mixed $expected
* @param mixed $actual
*/
function assertSame_($expected, $actual, string $label): void
{
global $failures, $passed;
if ($expected === $actual) {
$passed++;
echo " [OK] {$label}\n";
} else {
$failures++;
echo " [FAIL] {$label}\n";
echo ' expected: ' . var_export($expected, true) . "\n";
echo ' actual: ' . var_export($actual, true) . "\n";
}
}
function assertTrue_(bool $condition, string $label): void
{
assertSame_(true, $condition, $label);
}
// --- Test 1: defaults are used with no hints and no cookie -----------------
echo "Test 1: defaults with no signals\n";
$detector = new ThemePreferenceDetector([], [], []);
assertSame_('light', $detector->getPreference('color-scheme'), 'defaults to light');
assertTrue_($detector->isUsingDefaults(), 'isUsingDefaults() is true');
assertSame_(false, $detector->wasDetectedFromClientHints(), 'not from client hints');
assertSame_(false, $detector->wasDetectedFromCookie(), 'not from cookie');
// --- Test 2: custom defaults are respected ----------------------------------
echo "\nTest 2: custom defaults\n";
$detector = new ThemePreferenceDetector(['color-scheme' => 'dark'], [], []);
assertSame_('dark', $detector->getPreference('color-scheme'), 'custom default applied');
assertTrue_($detector->isDarkMode(), 'isDarkMode() true with custom default');
// --- Test 3: Client Hints take priority -------------------------------------
echo "\nTest 3: client hints detection\n";
$server = [
'HTTP_SEC_CH_PREFERS_COLOR_SCHEME' => 'dark',
'HTTP_SEC_CH_PREFERS_CONTRAST' => 'more',
'HTTP_SEC_CH_PREFERS_REDUCED_MOTION' => 'reduce',
];
$cookies = [
'tsync_prefs' => json_encode(['color-scheme' => 'light']),
];
$detector = new ThemePreferenceDetector([], $server, $cookies);
assertTrue_($detector->wasDetectedFromClientHints(), 'flagged as from client hints');
assertSame_(false, $detector->wasDetectedFromCookie(), 'cookie ignored when hints present');
assertTrue_($detector->isDarkMode(), 'dark mode from client hint, not cookie');
assertTrue_($detector->prefersMoreContrast(), 'more contrast from client hint');
assertTrue_($detector->prefersReducedMotion(), 'reduced motion from client hint');
assertSame_('no-preference', $detector->getPreference('reduced-transparency'), 'missing hint falls back to default');
// --- Test 4: cookie fallback when no Client Hints ---------------------------
echo "\nTest 4: cookie fallback\n";
$cookies = [
'tsync_prefs' => json_encode([
'color-scheme' => 'dark',
'reduced-motion' => 'reduce',
]),
];
$detector = new ThemePreferenceDetector([], [], $cookies);
assertTrue_($detector->wasDetectedFromCookie(), 'flagged as from cookie');
assertSame_(false, $detector->wasDetectedFromClientHints(), 'not flagged as from client hints');
assertTrue_($detector->isDarkMode(), 'dark mode from cookie');
assertTrue_($detector->prefersReducedMotion(), 'reduced motion from cookie');
assertSame_('no-preference', $detector->getPreference('contrast'), 'missing cookie key falls back to default');
// --- Test 5: malformed cookie is ignored gracefully -------------------------
echo "\nTest 5: malformed cookie\n";
$detector = new ThemePreferenceDetector([], [], ['tsync_prefs' => 'not-json{{{']);
assertTrue_($detector->isUsingDefaults(), 'malformed cookie falls back to defaults without error');
// --- Test 6: CSS output is well-formed and value-escaped --------------------
echo "\nTest 6: CSS variable output\n";
$server = ['HTTP_SEC_CH_PREFERS_COLOR_SCHEME' => 'dark; } body { color: red'];
$detector = new ThemePreferenceDetector([], $server, []);
$css = $detector->toCssVariables();
assertTrue_(str_contains($css, ':root {'), 'wraps in :root block');
assertTrue_(!str_contains($css, ';}'), 'injected characters stripped, no premature block close');
assertTrue_(!str_contains($css, '{ color'), 'CSS injection payload neutralized');
// --- Test 7: custom CSS variable mapping ------------------------------------
echo "\nTest 7: custom variable mapping\n";
$detector = new ThemePreferenceDetector(['color-scheme' => 'dark'], [], []);
$css = $detector->toCssVariables(['color-scheme' => '--app-theme']);
assertTrue_(str_contains($css, '--app-theme: dark;'), 'custom mapping used');
// --- Test 8: bootstrap script is valid-looking JS ---------------------------
echo "\nTest 8: bootstrap script\n";
$detector = new ThemePreferenceDetector();
$script = $detector->getBootstrapScript();
assertTrue_(str_starts_with($script, '<script>'), 'starts with <script>');
assertTrue_(str_ends_with($script, '</script>'), 'ends with </script>');
assertTrue_(str_contains($script, 'tsync_prefs'), 'references the cookie name');
assertTrue_(str_contains($script, 'matchMedia'), 'uses matchMedia');
echo "\n---\n{$passed} passed, {$failures} failed\n";
exit($failures > 0 ? 1 : 0);
|