*/ 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 */ 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 */ 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'], 'trailing space' => ['body_font_size', '15 ', 'erwartet eine Zahl'], 'trailing newline' => ['body_font_size', "15\n", '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', '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 */ 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', []); } }