area); self::assertSame( [ 'et_sanitize_font_choices()', 'et_sanitize_global_font_weight()', 'et_sanitize_float_number()', 'et_sanitize_int_number()', 'et_divi_global_font_style()', 'et_update_option()', 'et_delete_option()', 'ET_Core_PageResource::remove_static_resources()', 'et_pb_force_regenerate_templates()', 'et_pb_is_allowed()', ], array_map(static fn (Call $call): string => $call->label(), $manifest->calls) ); self::assertSame( [1, 1, 1, 1, 1, 2, 1, 2, 0, 1], array_map(static fn (Call $call): ?int => $call->args, $manifest->calls) ); self::assertSame(['et_divi'], $manifest->keys); self::assertSame([DiviFeature::ThemeOptionWrite], $manifest->features); self::assertCount(1, $manifest->mirrors); $mirror = $manifest->mirrors[0]; self::assertSame('customizer-typography', $mirror->id); self::assertSame('functions.php', $mirror->file); self::assertSame('et_divi_customizer_theme_settings', $mirror->function); self::assertNull($mirror->class); self::assertSame( [DiviFeature::ThemeOptionRead, DiviFeature::ThemeOptionWrite], $manifest->featuresOf($mirror) ); } public function testTheSanitizerTableCoversTheWhitelistWithDivisCallsOfTheManifest(): void { self::assertSame(self::KEYS, array_keys(ThemeOptionsAdapter::SANITIZERS)); $divi = array_values(array_unique(array_diff(ThemeOptionsAdapter::SANITIZERS, ['absint']))); sort($divi); self::assertSame( [ 'et_divi_global_font_style', 'et_sanitize_float_number', 'et_sanitize_font_choices', 'et_sanitize_global_font_weight', 'et_sanitize_int_number', ], $divi ); $calls = array_map( static fn (Call $call): string => $call->symbol . '/' . $call->args, ThemeOptionsAdapter::manifest()->calls ); foreach ($divi as $sanitizer) { self::assertContains($sanitizer . '/1', $calls, 'every Divi sanitizer is a call of the manifest'); } } public function testSanitizeRefusesAKeyOutsideTheWhitelist(): void { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('not a theme option of the whitelist: accent_color'); (new ThemeOptionsAdapter())->sanitize('accent_color', '#ff0000'); } public function testSizesGoThroughWordPressAbsintAndNonScalarsAreRefused(): void { $adapter = new ThemeOptionsAdapter(); self::assertSame(15, $adapter->sanitize('body_font_size', '15.7'), 'absint rounds, the caller compares'); self::assertSame(1200, $adapter->sanitize('content_width', 1200)); self::assertFalse($adapter->sanitize('row_padding', ['2'])); } /** @runInSeparateProcess */ public function testSanitizeCallsDivisCustomizerSanitizerOfTheKey(): void { Functions\expect('et_sanitize_font_choices')->once()->with('Roboto')->andReturn('Roboto'); Functions\expect('et_sanitize_global_font_weight')->once()->with('600')->andReturn('600'); Functions\expect('et_sanitize_float_number')->once()->with('1.8')->andReturn(1.8); Functions\expect('et_sanitize_int_number')->once()->with('-1')->andReturn(-1); Functions\expect('et_divi_global_font_style')->once()->with('italic|uppercase')->andReturn(false); $adapter = new ThemeOptionsAdapter(); self::assertSame('Roboto', $adapter->sanitize('secondary_nav_font', 'Roboto')); self::assertSame('600', $adapter->sanitize('body_font_weight', '600')); self::assertSame(1.8, $adapter->sanitize('body_header_height', '1.8')); self::assertSame(-1, $adapter->sanitize('body_header_spacing', '-1')); self::assertFalse($adapter->sanitize('body_header_style', 'italic|uppercase'), 'Divi refuses combinations'); } public function testReadReturnsThePresentWhitelistKeysWithTheirStoredTypes(): void { Functions\when('get_option')->alias(static fn (string $name): mixed => $name === 'et_divi' ? [ 'row_padding' => null, 'accent_color' => '#ff0000', 'body_font' => 'Roboto', 'body_font_height' => 1.8, 'gutter_width' => ['3'], 'heading_font' => false, 'body_font_size' => 15, ] : false); self::assertSame( ['heading_font' => false, 'body_font' => 'Roboto', 'body_font_size' => 15, 'body_font_height' => 1.8], (new ThemeOptionsAdapter())->read(), 'whitelist only, in W2 order; values of another type are not theme option values' ); } public function testReadWithoutThemeOptionsIsEmpty(): void { Functions\when('get_option')->justReturn(false); self::assertSame([], (new ThemeOptionsAdapter())->read()); } public function testWritesNeedAReleasedDivi(): void { ReleaseGate::useResolver( static fn (): ReleaseStatus => ReleaseMatrices::uniform(ReleaseState::Untested, '5.16.0') ); $this->expectException(DiviLocked::class); (new ThemeOptionsAdapter())->write(['body_font_size' => 16], []); } public function testWriteRefusesAKeyOutsideTheWhitelistBeforeWriting(): void { // Unit tests have no et_update_option(): reaching it would be an Error, not this exception. $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('not a theme option of the whitelist: accent_color, divi_logo'); (new ThemeOptionsAdapter())->write(['body_font_size' => 16, 'accent_color' => '#f00'], ['divi_logo']); } /** @runInSeparateProcess */ public function testWriteUpdatesAndDeletesPerKeyClearsTheCachesAndReadsBack(): void { $stored = ['body_font_size' => 14, 'heading_font' => 'Roboto', 'accent_color' => '#ff0000']; $log = []; Functions\when('get_option')->alias(static function (string $name) use (&$stored): mixed { return $name === 'et_divi' ? $stored : false; }); Functions\when('et_update_option')->alias(static function (string $key, mixed $value) use ( &$stored, &$log ): void { $stored[$key] = $value; $log[] = 'update ' . $key; }); Functions\when('et_delete_option')->alias(static function (string $key) use (&$stored, &$log): void { unset($stored[$key]); $log[] = 'delete ' . $key; }); Functions\when('et_pb_force_regenerate_templates')->alias(static function () use (&$log): void { $log[] = 'regenerate templates after ' . count(\ET_Core_PageResource::$removed) . ' static CSS clear'; }); \ET_Core_PageResource::$removed = []; $result = (new ThemeOptionsAdapter())->write( ['body_font_size' => 16, 'body_font_height' => 1.8], ['heading_font'] ); self::assertSame(['body_font_size' => 16, 'body_font_height' => 1.8], $result); self::assertSame( [ 'update body_font_size', 'update body_font_height', 'delete heading_font', 'regenerate templates after 1 static CSS clear', ], $log ); self::assertSame([['all', 'all', false]], \ET_Core_PageResource::$removed, 'like a customizer save'); self::assertSame('#ff0000', $stored['accent_color'], 'other theme options stay'); } /** @runInSeparateProcess */ public function testWriteThrowsWhenTheReadBackDiffers(): void { $stored = ['body_font_size' => 14, 'heading_font' => 'Roboto']; Functions\when('get_option')->alias(static fn (string $name): mixed => $name === 'et_divi' ? $stored : false); // Divi silently saves nothing (e.g. a filter or another process). Functions\when('et_update_option')->justReturn(null); Functions\when('et_delete_option')->justReturn(null); Functions\when('et_pb_force_regenerate_templates')->justReturn(null); $this->expectException(RuntimeException::class); $this->expectExceptionMessage('theme options not written: body_font_size, heading_font'); (new ThemeOptionsAdapter())->write(['body_font_size' => 16], ['heading_font']); } public function testWithoutDiviNobodyCustomizesTheTheme(): void { // Unit tests have no et_pb_is_allowed(): a missing Divi function never means "allowed". self::assertFalse((new ThemeOptionsAdapter())->canCustomize()); } /** @runInSeparateProcess */ public function testCanCustomizeAsksTheThemeCustomizerRight(): void { Functions\when('et_pb_is_allowed')->alias(static fn (string $cap): bool => $cap === 'theme_customizer'); self::assertTrue((new ThemeOptionsAdapter())->canCustomize()); } }