Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
291 lines
16 KiB
PHP
291 lines
16 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Design;
|
|
|
|
use InvalidArgumentException;
|
|
use LIT\MCP\Integrations\Divi\Design\ThemeOptionSpec;
|
|
use LIT\MCP\Integrations\Divi\Design\ThemeOptionsAdapter;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
final class ThemeOptionSpecTest extends TestCase
|
|
{
|
|
public function testTheTwentyKeysKeepTheOrderOfRulingW2(): void
|
|
{
|
|
self::assertSame([
|
|
'heading_font', 'body_font', 'primary_nav_font', 'secondary_nav_font',
|
|
'heading_font_weight', 'body_font_weight',
|
|
'body_font_size', 'tablet_body_font_size', 'phone_body_font_size',
|
|
'body_header_size', 'tablet_header_font_size', 'phone_header_font_size',
|
|
'body_font_height', 'body_header_height', 'body_header_spacing', 'body_header_style',
|
|
'content_width', 'gutter_width', 'section_padding', 'row_padding',
|
|
], ThemeOptionSpec::KEYS);
|
|
}
|
|
|
|
public function testTheKeysAreTheKeysOfTheAdapterSanitizersInTheSameOrder(): void
|
|
{
|
|
self::assertSame(array_keys(ThemeOptionsAdapter::SANITIZERS), ThemeOptionSpec::KEYS);
|
|
}
|
|
|
|
/** @return array<string, array{string, string, int|float|string|null, int|float|null, int|float|null}> */
|
|
public static function definitions(): array
|
|
{
|
|
return [
|
|
'heading_font' => ['heading_font', 'font', null, null, null],
|
|
'body_font' => ['body_font', 'font', null, null, null],
|
|
'primary_nav_font' => ['primary_nav_font', 'font', null, null, null],
|
|
'secondary_nav_font' => ['secondary_nav_font', 'font', null, null, null],
|
|
'heading_font_weight' => ['heading_font_weight', 'weight', '500', null, null],
|
|
'body_font_weight' => ['body_font_weight', 'weight', '500', null, null],
|
|
'body_font_size' => ['body_font_size', 'int', 14, 10, 32],
|
|
'tablet_body_font_size' => ['tablet_body_font_size', 'int', 14, 10, 32],
|
|
'phone_body_font_size' => ['phone_body_font_size', 'int', 14, 10, 32],
|
|
'body_header_size' => ['body_header_size', 'int', 30, 22, 72],
|
|
'tablet_header_font_size' => ['tablet_header_font_size', 'int', 30, 22, 72],
|
|
'phone_header_font_size' => ['phone_header_font_size', 'int', 30, 22, 72],
|
|
'body_font_height' => ['body_font_height', 'float', 1.7, 0.8, 3],
|
|
'body_header_height' => ['body_header_height', 'float', 1.0, 0.8, 3],
|
|
'body_header_spacing' => ['body_header_spacing', 'int', 0, -2, 10],
|
|
'body_header_style' => ['body_header_style', 'style', null, null, null],
|
|
'content_width' => ['content_width', 'int', 1080, 960, 1920],
|
|
'gutter_width' => ['gutter_width', 'int', 3, 1, 4],
|
|
'section_padding' => ['section_padding', 'int', 4, 0, 10],
|
|
'row_padding' => ['row_padding', 'int', 2, 0, 10],
|
|
];
|
|
}
|
|
|
|
/** @dataProvider definitions */
|
|
public function testEveryKeyHasItsDefinition(
|
|
string $key,
|
|
string $type,
|
|
mixed $default,
|
|
mixed $min,
|
|
mixed $max
|
|
): void
|
|
{
|
|
$definition = ThemeOptionSpec::definition($key);
|
|
|
|
self::assertTrue(ThemeOptionSpec::isKey($key));
|
|
self::assertSame($type, $definition['type']);
|
|
self::assertSame($default, $definition['default']);
|
|
self::assertNotSame('', $definition['label']);
|
|
self::assertSame($min, $definition['min'] ?? null);
|
|
self::assertSame($max, $definition['max'] ?? null);
|
|
self::assertSame(in_array($type, ['int', 'float'], true), isset($definition['step']));
|
|
self::assertSame(in_array($type, ['weight', 'style'], true), isset($definition['choices']));
|
|
}
|
|
|
|
public function testInheritanceAndChoicesAreDefined(): void
|
|
{
|
|
self::assertSame('body_font_size', ThemeOptionSpec::definition('tablet_body_font_size')['inherits']);
|
|
self::assertSame('tablet_body_font_size', ThemeOptionSpec::definition('phone_body_font_size')['inherits']);
|
|
self::assertSame('body_header_size', ThemeOptionSpec::definition('tablet_header_font_size')['inherits']);
|
|
self::assertSame('tablet_header_font_size', ThemeOptionSpec::definition('phone_header_font_size')['inherits']);
|
|
self::assertArrayNotHasKey('inherits', ThemeOptionSpec::definition('body_font_size'));
|
|
self::assertSame(
|
|
['100', '200', '300', '400', '500', '600', '700', '800', '900'],
|
|
ThemeOptionSpec::definition('body_font_weight')['choices']
|
|
);
|
|
self::assertSame(
|
|
['italic', 'uppercase', 'underline'],
|
|
ThemeOptionSpec::definition('body_header_style')['choices']
|
|
);
|
|
self::assertSame(0.1, ThemeOptionSpec::definition('body_font_height')['step']);
|
|
}
|
|
|
|
public function testAnUnknownKeyIsNoKeyAndHasNoDefinition(): void
|
|
{
|
|
self::assertFalse(ThemeOptionSpec::isKey('accent_color'));
|
|
$this->expectException(InvalidArgumentException::class);
|
|
ThemeOptionSpec::definition('accent_color');
|
|
}
|
|
|
|
/** @return array<string, array{string, mixed}> */
|
|
public static function validValues(): array
|
|
{
|
|
return [
|
|
'int lower bound' => ['body_font_size', 10],
|
|
'int upper bound' => ['body_font_size', 32],
|
|
'int as digits' => ['body_font_size', '15'],
|
|
'int as whole float' => ['body_font_size', 15.0],
|
|
'negative int bound' => ['body_header_spacing', -2],
|
|
'negative digits' => ['body_header_spacing', '-1'],
|
|
'width bounds' => ['content_width', 1920],
|
|
'zero padding' => ['section_padding', 0],
|
|
'float lower bound' => ['body_font_height', 0.8],
|
|
'float upper bound as int' => ['body_font_height', 3],
|
|
'float on a step' => ['body_font_height', 1.7],
|
|
'float as digits' => ['body_header_height', '1.2'],
|
|
'float from int' => ['body_header_height', 1],
|
|
'font' => ['heading_font', 'Open Sans'],
|
|
'font with umlaut and padding' => ['body_font', " Fjalla Önö \t"],
|
|
'font of 100 characters' => ['primary_nav_font', str_repeat('a', 100)],
|
|
'weight as text' => ['heading_font_weight', '700'],
|
|
'weight as int' => ['body_font_weight', 400],
|
|
'style' => ['body_header_style', 'uppercase'],
|
|
];
|
|
}
|
|
|
|
/** @dataProvider validValues */
|
|
public function testCheckAcceptsValidValues(string $key, mixed $value): void
|
|
{
|
|
self::assertNull(ThemeOptionSpec::check($key, $value));
|
|
}
|
|
|
|
/** @return array<string, array{string, mixed, string}> */
|
|
public static function invalidValues(): array
|
|
{
|
|
return [
|
|
'int below' => ['body_font_size', 9, 'außerhalb von 10 bis 32'],
|
|
'int above' => ['body_font_size', 33, 'außerhalb von 10 bis 32'],
|
|
'int digits above' => ['content_width', '1921', 'außerhalb von 960 bis 1920'],
|
|
'negative below' => ['body_header_spacing', -3, 'außerhalb von -2 bis 10'],
|
|
'float with a fraction for an int key' => ['body_font_size', 15.5, 'ganze Zahl'],
|
|
'digits with a fraction for an int key' => ['row_padding', '2.5', 'ganze Zahl'],
|
|
'non-numeric text' => ['body_font_size', 'groß', 'erwartet eine Zahl, erhalten "groß"'],
|
|
'unit suffix' => ['body_font_size', '15px', 'erwartet eine Zahl'],
|
|
'exponent' => ['body_font_size', '1e1', 'erwartet eine Zahl'],
|
|
'spaces around digits' => ['body_font_size', ' 15', 'erwartet eine Zahl'],
|
|
'empty text' => ['gutter_width', '', 'erwartet eine Zahl'],
|
|
'true' => ['body_font_size', true, 'erhalten true'],
|
|
'false' => ['body_font_size', false, 'erhalten false'],
|
|
'null' => ['body_font_size', null, 'erhalten null'],
|
|
'array' => ['body_font_size', [14], 'erhalten einen array'],
|
|
'infinity' => ['body_font_height', INF, 'erwartet eine Zahl'],
|
|
'not a number' => ['body_font_height', NAN, 'erwartet eine Zahl'],
|
|
'float below' => ['body_font_height', 0.7, 'außerhalb von 0,8 bis 3'],
|
|
'float above' => ['body_header_height', 3.1, 'außerhalb von 0,8 bis 3'],
|
|
'float off the step' => ['body_font_height', 1.75, 'Schrittweite 0,1 (ab 0,8)'],
|
|
'float digits off the step' => ['body_header_height', '1.05', 'Schrittweite'],
|
|
'font as number' => ['heading_font', 12, 'erwartet einen Schriftnamen'],
|
|
'font as false' => ['heading_font', false, 'erwartet einen Schriftnamen'],
|
|
'font as null' => ['heading_font', null, 'erhalten null'],
|
|
'empty font' => ['body_font', '', '1 bis 100 Zeichen'],
|
|
'blank font' => ['body_font', " \t ", '1 bis 100 Zeichen'],
|
|
'font of 101 characters' => ['body_font', str_repeat('a', 101), '1 bis 100 Zeichen'],
|
|
'font with a control character' => ['body_font', "Open\nSans", 'unzulässige Zeichen'],
|
|
'font with angle brackets' => ['body_font', 'Open<Sans>', 'unzulässige Zeichen'],
|
|
'font with a quote' => ['primary_nav_font', 'Open "Sans"', 'unzulässige Zeichen'],
|
|
'font with an apostrophe' => ['secondary_nav_font', "Open'Sans", 'unzulässige Zeichen'],
|
|
'font with invalid utf-8' => ['body_font', "Open\xC3(Sans", 'unzulässige Zeichen'],
|
|
'font none' => ['heading_font', 'None', 'null setzt auf Divis Standardschrift zurück'],
|
|
'weight off the list' => ['body_font_weight', '550', 'erlaubt ist genau ein Wert aus 100, 200'],
|
|
'weight as float' => ['body_font_weight', 500.0, 'erhalten 500'],
|
|
'weight word' => ['heading_font_weight', 'bold', 'erhalten "bold"'],
|
|
'weight as null' => ['heading_font_weight', null, 'erhalten null'],
|
|
'style combination' => ['body_header_style', 'italic|uppercase', 'genau ein Wert'],
|
|
'style empty' => ['body_header_style', '', 'genau ein Wert'],
|
|
'style bold' => ['body_header_style', 'bold', 'genau ein Wert'],
|
|
'style case' => ['body_header_style', 'Italic', 'genau ein Wert'],
|
|
'style as list' => ['body_header_style', ['italic'], 'erhalten einen array'],
|
|
];
|
|
}
|
|
|
|
/** @dataProvider invalidValues */
|
|
public function testCheckRejectsInvalidValuesWithAGermanMessage(string $key, mixed $value, string $needle): void
|
|
{
|
|
$message = ThemeOptionSpec::check($key, $value);
|
|
|
|
self::assertIsString($message);
|
|
self::assertStringContainsString($needle, $message);
|
|
self::assertStringStartsWith(ThemeOptionSpec::definition($key)['label'] . ':', $message);
|
|
}
|
|
|
|
/** @return array<string, array{string}> */
|
|
public static function everyKey(): array
|
|
{
|
|
return array_map(static fn (string $key): array => [$key], array_combine(
|
|
ThemeOptionSpec::KEYS,
|
|
ThemeOptionSpec::KEYS
|
|
));
|
|
}
|
|
|
|
/**
|
|
* The adapter's sanitizers let booleans through is_scalar (absint(true) = 1), so check() must stop them.
|
|
*
|
|
* @dataProvider everyKey
|
|
*/
|
|
public function testCheckRejectsBooleansNullArraysAndObjectsForEveryKey(string $key): void
|
|
{
|
|
foreach ([true, false, null, [], [1], new \stdClass()] as $value) {
|
|
self::assertIsString(ThemeOptionSpec::check($key, $value), $key . ' accepted ' . get_debug_type($value));
|
|
}
|
|
}
|
|
|
|
public function testCheckOfAnUnknownKeyThrows(): void
|
|
{
|
|
$this->expectException(InvalidArgumentException::class);
|
|
ThemeOptionSpec::check('logo', 'x');
|
|
}
|
|
|
|
public function testNormalizeCastsToTheTypeOfTheKey(): void
|
|
{
|
|
self::assertSame(15, ThemeOptionSpec::normalize('body_font_size', '15'));
|
|
self::assertSame(15, ThemeOptionSpec::normalize('body_font_size', 15.0));
|
|
self::assertSame(-1, ThemeOptionSpec::normalize('body_header_spacing', '-1'));
|
|
self::assertSame(1.7, ThemeOptionSpec::normalize('body_font_height', '1.7'));
|
|
self::assertSame(1.0, ThemeOptionSpec::normalize('body_header_height', 1));
|
|
self::assertSame('700', ThemeOptionSpec::normalize('heading_font_weight', 700));
|
|
self::assertSame('400', ThemeOptionSpec::normalize('body_font_weight', '400'));
|
|
self::assertSame('Open Sans', ThemeOptionSpec::normalize('heading_font', " Open Sans\t"));
|
|
self::assertSame('italic', ThemeOptionSpec::normalize('body_header_style', 'italic'));
|
|
}
|
|
|
|
public function testEffectiveFallsBackToTheDivisDefaultsWhenNothingIsStored(): void
|
|
{
|
|
self::assertSame(14, ThemeOptionSpec::effective('body_font_size', []));
|
|
self::assertSame(1.7, ThemeOptionSpec::effective('body_font_height', []));
|
|
self::assertSame('500', ThemeOptionSpec::effective('heading_font_weight', []));
|
|
self::assertSame(1080, ThemeOptionSpec::effective('content_width', []));
|
|
self::assertNull(ThemeOptionSpec::effective('heading_font', []));
|
|
self::assertNull(ThemeOptionSpec::effective('body_header_style', []));
|
|
self::assertSame(14, ThemeOptionSpec::effective('phone_body_font_size', []));
|
|
self::assertSame(30, ThemeOptionSpec::effective('phone_header_font_size', []));
|
|
}
|
|
|
|
public function testEffectiveInheritsTheNextLargerSizeWhileUnset(): void
|
|
{
|
|
$stored = ['body_font_size' => 18, 'body_header_size' => 40];
|
|
|
|
self::assertSame(18, ThemeOptionSpec::effective('tablet_body_font_size', $stored));
|
|
self::assertSame(18, ThemeOptionSpec::effective('phone_body_font_size', $stored));
|
|
self::assertSame(40, ThemeOptionSpec::effective('tablet_header_font_size', $stored));
|
|
self::assertSame(40, ThemeOptionSpec::effective('phone_header_font_size', $stored));
|
|
}
|
|
|
|
public function testAStoredOverrideBreaksTheChainAtItsLevelOnly(): void
|
|
{
|
|
$stored = ['body_font_size' => 18, 'tablet_body_font_size' => 16, 'tablet_header_font_size' => 28];
|
|
|
|
self::assertSame(18, ThemeOptionSpec::effective('body_font_size', $stored));
|
|
self::assertSame(16, ThemeOptionSpec::effective('tablet_body_font_size', $stored));
|
|
self::assertSame(16, ThemeOptionSpec::effective('phone_body_font_size', $stored));
|
|
self::assertSame(28, ThemeOptionSpec::effective('phone_header_font_size', $stored));
|
|
self::assertSame(30, ThemeOptionSpec::effective('body_header_size', $stored));
|
|
$stored['phone_body_font_size'] = '12';
|
|
self::assertSame(12, ThemeOptionSpec::effective('phone_body_font_size', $stored));
|
|
}
|
|
|
|
public function testEffectiveTreatsFalseAndGarbageAsUnsetAndCastsStoredDigits(): void
|
|
{
|
|
self::assertNull(ThemeOptionSpec::effective('heading_font', ['heading_font' => false]));
|
|
self::assertNull(ThemeOptionSpec::effective('heading_font', ['heading_font' => 'none']));
|
|
self::assertSame('Lato', ThemeOptionSpec::effective('heading_font', ['heading_font' => 'Lato']));
|
|
self::assertSame(14, ThemeOptionSpec::effective('body_font_size', ['body_font_size' => 'groß']));
|
|
self::assertSame(15, ThemeOptionSpec::effective('body_font_size', ['body_font_size' => '15']));
|
|
self::assertSame(1.2, ThemeOptionSpec::effective('body_font_height', ['body_font_height' => '1.2']));
|
|
self::assertSame('300', ThemeOptionSpec::effective('body_font_weight', ['body_font_weight' => 300]));
|
|
self::assertSame(40, ThemeOptionSpec::effective('body_font_size', ['body_font_size' => 40]), 'no range check');
|
|
self::assertSame(18, ThemeOptionSpec::effective('tablet_body_font_size', [
|
|
'body_font_size' => 18, 'tablet_body_font_size' => false,
|
|
]));
|
|
}
|
|
|
|
public function testEffectiveOfAnUnknownKeyThrows(): void
|
|
{
|
|
$this->expectException(InvalidArgumentException::class);
|
|
ThemeOptionSpec::effective('nope', []);
|
|
}
|
|
}
|