From de8756561b0c25e22ff9fa7cbb3f29f7735b408c Mon Sep 17 00:00:00 2001 From: Louis Wolmarans Date: Wed, 20 May 2026 21:42:48 +0200 Subject: [PATCH 1/4] Unified Snippets: implement Tier 3 scanner - Divi --- src/php/Plugin.php | 2 + .../Scanners/Divi_Theme_Options_Scanner.php | 389 ++++++++++++++++++ tests/phpunit/test-divi-scanner.php | 216 ++++++++++ 3 files changed, 607 insertions(+) create mode 100644 src/php/UnifiedSnippets/Scanners/Divi_Theme_Options_Scanner.php create mode 100644 tests/phpunit/test-divi-scanner.php diff --git a/src/php/Plugin.php b/src/php/Plugin.php index 24598976..ade1ecd3 100644 --- a/src/php/Plugin.php +++ b/src/php/Plugin.php @@ -22,6 +22,7 @@ use Code_Snippets\UnifiedSnippets\Scan_Results_Store; use Code_Snippets\UnifiedSnippets\REST\Scan_REST_Controller; use Code_Snippets\UnifiedSnippets\Scanners\Additional_CSS_Scanner; +use Code_Snippets\UnifiedSnippets\Scanners\Divi_Theme_Options_Scanner; use Code_Snippets\UnifiedSnippets\Scanners\Functions_Php_Scanner; use Code_Snippets\UnifiedSnippets\Scanners\Header_Footer_Code_Manager_Scanner; use Code_Snippets\UnifiedSnippets\Scanners\Htaccess_Scanner; @@ -197,6 +198,7 @@ private function init_unified_snippets(): void { $this->unified_snippets->register( new Insert_Headers_And_Footers_Scanner() ); $this->unified_snippets->register( new Header_Footer_Code_Manager_Scanner() ); $this->unified_snippets->register( new Insert_PHP_Code_Snippet_Scanner() ); + $this->unified_snippets->register( new Divi_Theme_Options_Scanner() ); new Scan_REST_Controller( $this->unified_snippets, $this->unified_snippets_store ); } diff --git a/src/php/UnifiedSnippets/Scanners/Divi_Theme_Options_Scanner.php b/src/php/UnifiedSnippets/Scanners/Divi_Theme_Options_Scanner.php new file mode 100644 index 00000000..acde900e --- /dev/null +++ b/src/php/UnifiedSnippets/Scanners/Divi_Theme_Options_Scanner.php @@ -0,0 +1,389 @@ +` array (one-row mode) or per-key options like `et__custom_css` + * (per-row mode). This scanner mirrors Divi's own `et_get_option()` read path so it works in + * either layout, and surfaces each populated field as a {@see Discovered_Snippet}. + * + * The four Integration fields each have a companion enable toggle (e.g. `_integrate_header_enable`) + * that Divi checks before emitting the code at runtime. We honour those toggles via `is_active` + * so the Unified view reflects what is actually running. + * + * Wrapping logic for the post-scoped fields (which fire on the Divi-only `et_before_post` / + * `et_after_post` hooks) is intentionally left to a later Phase 3 importer; the scanner stays + * read-only and records the relevant caveat in `import_notes`. + * + * @package Code_Snippets + */ +class Divi_Theme_Options_Scanner extends Scanner_Base { + + /** + * Templates whose option storage this scanner understands. + */ + private const SUPPORTED_TEMPLATES = [ 'Divi', 'Extra' ]; + + /** + * Default shortname used when the active template is not one of the supported themes + * (only relevant in override-mode for tests). + */ + private const DEFAULT_SHORTNAME = 'divi'; + + /** + * Optional in-memory overrides keyed by Divi option name (without the `et__` prefix). + * + * Used by tests to inject fixture values without touching wp_options. When non-empty, the + * scanner also treats itself as available regardless of the active template so test fixtures + * can drive end-to-end behaviour. + * + * @var array + */ + private array $option_overrides; + + /** + * Class constructor. + * + * @param array $option_overrides Optional option-value overrides for testing. + */ + public function __construct( array $option_overrides = [] ) { + $this->option_overrides = $option_overrides; + } + + /** + * {@inheritDoc} + */ + public function get_id(): string { + return 'divi-theme-options'; + } + + /** + * {@inheritDoc} + */ + public function get_label(): string { + return __( 'Divi Theme Options', 'code-snippets' ); + } + + /** + * {@inheritDoc} + * + * Available only when Divi (or Extra) is the active theme template. We deliberately do + * not scan when Divi is merely installed: its hooks are not running, so the code is + * dormant and the namespaced option keys belong to an inactive theme. + */ + public function is_available(): bool { + if ( $this->option_overrides ) { + return true; + } + + if ( ! function_exists( 'get_template' ) ) { + return false; + } + + return in_array( get_template(), self::SUPPORTED_TEMPLATES, true ); + } + + /** + * {@inheritDoc} + * + * Raw HTML/JS is injected unfiltered into every page (head/footer) or every single post + * (top/bottom); CSS overrides the active theme's styles. Medium captures that risk without + * implying that wp-config-level damage is possible. + */ + public function get_risk_level(): string { + return 'medium'; + } + + /** + * {@inheritDoc} + */ + public function supports_import(): bool { + return true; + } + + /** + * {@inheritDoc} + */ + public function scan(): array { + $shortname = $this->resolve_shortname(); + $source_name = ucfirst( $shortname ); + + $snippets = [ + $this->scan_custom_css( $shortname, $source_name ), + $this->scan_integration_head( $shortname, $source_name ), + $this->scan_integration_body( $shortname, $source_name ), + $this->scan_integration_single_top( $shortname, $source_name ), + $this->scan_integration_single_bottom( $shortname, $source_name ), + ]; + + return array_values( array_filter( $snippets ) ); + } + + /** + * Resolve the Divi option shortname for the active template. + * + * @return string 'divi' or 'extra'. + */ + private function resolve_shortname(): string { + if ( ! function_exists( 'get_template' ) ) { + return self::DEFAULT_SHORTNAME; + } + + $template = get_template(); + + if ( 'Extra' === $template ) { + return 'extra'; + } + + return self::DEFAULT_SHORTNAME; + } + + /** + * Read a Divi option, mirroring `et_get_option()`'s two storage modes. + * + * Divi may store its theme options either as a single serialised array under `et_` + * (one-row mode) or as individual options keyed `et__` (per-row mode). The + * actual mode is decided per-install by `et_options_stored_in_one_row()`. Rather than load + * that helper, we check both locations and return whichever yields a value; reads against an + * empty install harmlessly return ''. + * + * @param string $shortname The resolved Divi shortname. + * @param string $key Option key without the `et__` prefix (e.g. `custom_css`). + * + * @return string The stored value, or '' when unset. + */ + private function read_option( string $shortname, string $key ): string { + if ( array_key_exists( $key, $this->option_overrides ) ) { + return (string) $this->option_overrides[ $key ]; + } + + if ( ! function_exists( 'get_option' ) ) { + return ''; + } + + $bundle = get_option( 'et_' . $shortname ); + if ( is_array( $bundle ) && isset( $bundle[ $key ] ) && '' !== (string) $bundle[ $key ] ) { + return (string) $bundle[ $key ]; + } + + $value = get_option( 'et_' . $shortname . '_' . $key, '' ); + + return is_scalar( $value ) ? (string) $value : ''; + } + + /** + * Read one of Divi's `_integrate_*_enable` toggles. + * + * Divi treats these as 'on' by default when unset, matching the checkbox `std` values in + * options_divi.php. We do the same so a freshly populated Integration field is reported as + * active rather than silently inactive. + * + * @param string $shortname The resolved Divi shortname. + * @param string $key Toggle key without the `et__` prefix. + * + * @return bool + */ + private function read_toggle( string $shortname, string $key ): bool { + $value = $this->read_option( $shortname, $key ); + + if ( '' === $value ) { + return true; + } + + return 'on' === $value; + } + + /** + * Build the Custom CSS snippet, if populated. + * + * @param string $shortname The Divi option shortname. + * @param string $source_name Human-readable theme name. + * + * @return Discovered_Snippet|null + */ + private function scan_custom_css( string $shortname, string $source_name ): ?Discovered_Snippet { + $code = $this->read_option( $shortname, 'custom_css' ); + + if ( '' === trim( $code ) ) { + return null; + } + + return $this->build_field_snippet( + [ + 'name' => sprintf( + /* translators: %s: theme name (e.g. Divi). */ + __( '%s Custom CSS', 'code-snippets' ), + $source_name + ), + 'code' => $code, + 'type' => 'css', + 'source_name' => $source_name, + 'source_path' => 'divi://theme-options/custom_css', + 'is_active' => true, + 'import_notes' => __( 'Imports cleanly as a site-wide CSS snippet (scope: site-css).', 'code-snippets' ), + ] + ); + } + + /** + * Build the Integration > Head snippet, if populated. + * + * @param string $shortname The Divi option shortname. + * @param string $source_name Human-readable theme name. + * + * @return Discovered_Snippet|null + */ + private function scan_integration_head( string $shortname, string $source_name ): ?Discovered_Snippet { + $code = $this->read_option( $shortname, 'integration_head' ); + + if ( '' === trim( $code ) ) { + return null; + } + + return $this->build_field_snippet( + [ + 'name' => sprintf( + /* translators: %s: theme name (e.g. Divi). */ + __( '%s Head Code', 'code-snippets' ), + $source_name + ), + 'code' => $code, + 'type' => 'html', + 'source_name' => $source_name, + 'source_path' => 'divi://theme-options/integration_head', + 'is_active' => $this->read_toggle( $shortname, 'integrate_header_enable' ), + 'import_notes' => __( 'Imports into the head-content scope (rendered on wp_head).', 'code-snippets' ), + ] + ); + } + + /** + * Build the Integration > Body snippet, if populated. + * + * Despite Divi's UI labelling this "body" code, the theme actually emits the content on + * `wp_footer` at priority 12 (see Divi/epanel/custom_functions.php). Importing into + * Code Snippets' `footer-content` scope (also wp_footer, priority 20) reproduces the + * runtime behaviour. The priority delta is surfaced in `import_notes` for users with + * order-sensitive scripts. + * + * @param string $shortname The Divi option shortname. + * @param string $source_name Human-readable theme name. + * + * @return Discovered_Snippet|null + */ + private function scan_integration_body( string $shortname, string $source_name ): ?Discovered_Snippet { + $code = $this->read_option( $shortname, 'integration_body' ); + + if ( '' === trim( $code ) ) { + return null; + } + + return $this->build_field_snippet( + [ + 'name' => sprintf( + /* translators: %s: theme name (e.g. Divi). */ + __( '%s Body Code', 'code-snippets' ), + $source_name + ), + 'code' => $code, + 'type' => 'html', + 'source_name' => $source_name, + 'source_path' => 'divi://theme-options/integration_body', + 'is_active' => $this->read_toggle( $shortname, 'integrate_body_enable' ), + 'import_notes' => __( 'Divi renders this on wp_footer at priority 12. Importing as footer-content runs it on wp_footer at priority 20, which may matter for order-sensitive scripts.', 'code-snippets' ), + ] + ); + } + + /** + * Build the Integration > Single Top snippet, if populated. + * + * @param string $shortname The Divi option shortname. + * @param string $source_name Human-readable theme name. + * + * @return Discovered_Snippet|null + */ + private function scan_integration_single_top( string $shortname, string $source_name ): ?Discovered_Snippet { + $code = $this->read_option( $shortname, 'integration_single_top' ); + + if ( '' === trim( $code ) ) { + return null; + } + + return $this->build_field_snippet( + [ + 'name' => sprintf( + /* translators: %s: theme name (e.g. Divi). */ + __( '%s Single Top Code', 'code-snippets' ), + $source_name + ), + 'code' => $code, + 'type' => 'html', + 'source_name' => $source_name, + 'source_path' => 'divi://theme-options/integration_single_top', + 'is_active' => $this->read_toggle( $shortname, 'integrate_singletop_enable' ), + 'import_notes' => __( 'Fires on Divi\'s et_before_post hook. Import wraps the code in an add_action() targeting that hook, so the snippet will only run while the Divi theme is active.', 'code-snippets' ), + ] + ); + } + + /** + * Build the Integration > Single Bottom snippet, if populated. + * + * @param string $shortname The Divi option shortname. + * @param string $source_name Human-readable theme name. + * + * @return Discovered_Snippet|null + */ + private function scan_integration_single_bottom( string $shortname, string $source_name ): ?Discovered_Snippet { + $code = $this->read_option( $shortname, 'integration_single_bottom' ); + + if ( '' === trim( $code ) ) { + return null; + } + + return $this->build_field_snippet( + [ + 'name' => sprintf( + /* translators: %s: theme name (e.g. Divi). */ + __( '%s Single Bottom Code', 'code-snippets' ), + $source_name + ), + 'code' => $code, + 'type' => 'html', + 'source_name' => $source_name, + 'source_path' => 'divi://theme-options/integration_single_bottom', + 'is_active' => $this->read_toggle( $shortname, 'integrate_singlebottom_enable' ), + 'import_notes' => __( 'Fires on Divi\'s et_after_post hook. Import wraps the code in an add_action() targeting that hook, so the snippet will only run while the Divi theme is active.', 'code-snippets' ), + ] + ); + } + + /** + * Common field defaults applied to every Divi snippet before delegating to build_snippet(). + * + * @param array $fields Per-field overrides. + * + * @return Discovered_Snippet + */ + private function build_field_snippet( array $fields ): Discovered_Snippet { + return $this->build_snippet( + array_merge( + [ + 'source_type' => 'theme', + 'line_start' => 0, + 'line_end' => 0, + ], + $fields + ) + ); + } +} diff --git a/tests/phpunit/test-divi-scanner.php b/tests/phpunit/test-divi-scanner.php new file mode 100644 index 00000000..17eb41cd --- /dev/null +++ b/tests/phpunit/test-divi-scanner.php @@ -0,0 +1,216 @@ + + */ + private const FULL_FIXTURE = [ + 'custom_css' => '.divi-test { color: red; }', + 'integration_head' => '', + 'integration_body' => '', + 'integration_single_top' => '
Top
', + 'integration_single_bottom' => '
Bottom
', + 'integrate_header_enable' => 'on', + 'integrate_body_enable' => 'on', + 'integrate_singletop_enable' => 'on', + 'integrate_singlebottom_enable' => 'on', + ]; + + /** + * Index a scan result by snippet name for easier per-field assertions. + * + * @param array $results Scanner output. + * + * @return array + */ + private function index_by_name( array $results ): array { + $indexed = []; + + foreach ( $results as $snippet ) { + $indexed[ $snippet->name ] = $snippet; + } + + return $indexed; + } + + /** + * A populated fixture produces one Discovered_Snippet per Divi field with the expected + * type, source metadata, and active flags. + */ + public function test_scan_returns_one_snippet_per_populated_field() { + $scanner = new Divi_Theme_Options_Scanner( self::FULL_FIXTURE ); + + $this->assertTrue( $scanner->is_available() ); + + $results = $scanner->scan(); + $this->assertCount( 5, $results ); + + $by_name = $this->index_by_name( $results ); + + $this->assertArrayHasKey( 'Divi Custom CSS', $by_name ); + $this->assertSame( 'css', $by_name['Divi Custom CSS']->type ); + $this->assertSame( '.divi-test { color: red; }', $by_name['Divi Custom CSS']->code ); + $this->assertSame( 'divi://theme-options/custom_css', $by_name['Divi Custom CSS']->source_path ); + $this->assertTrue( $by_name['Divi Custom CSS']->is_active ); + + $this->assertArrayHasKey( 'Divi Head Code', $by_name ); + $this->assertSame( 'html', $by_name['Divi Head Code']->type ); + $this->assertSame( 'divi://theme-options/integration_head', $by_name['Divi Head Code']->source_path ); + $this->assertTrue( $by_name['Divi Head Code']->is_active ); + + $this->assertArrayHasKey( 'Divi Body Code', $by_name ); + $this->assertSame( 'html', $by_name['Divi Body Code']->type ); + $this->assertStringContainsString( 'wp_footer', $by_name['Divi Body Code']->import_notes ); + $this->assertTrue( $by_name['Divi Body Code']->is_active ); + + $this->assertArrayHasKey( 'Divi Single Top Code', $by_name ); + $this->assertStringContainsString( 'et_before_post', $by_name['Divi Single Top Code']->import_notes ); + $this->assertTrue( $by_name['Divi Single Top Code']->is_active ); + + $this->assertArrayHasKey( 'Divi Single Bottom Code', $by_name ); + $this->assertStringContainsString( 'et_after_post', $by_name['Divi Single Bottom Code']->import_notes ); + $this->assertTrue( $by_name['Divi Single Bottom Code']->is_active ); + + foreach ( $results as $snippet ) { + $this->assertSame( 'theme', $snippet->source_type ); + $this->assertSame( 'Divi', $snippet->source_name ); + $this->assertSame( 'divi-theme-options', $snippet->scanner_id ); + $this->assertSame( 'medium', $snippet->risk_level ); + $this->assertTrue( $snippet->is_importable ); + $this->assertSame( 0, $snippet->line_start ); + $this->assertSame( 0, $snippet->line_end ); + } + } + + /** + * Empty and whitespace-only fields are skipped, so a sparsely populated install only + * surfaces the fields the user has actually filled in. + */ + public function test_scan_skips_empty_fields() { + $scanner = new Divi_Theme_Options_Scanner( + [ + 'custom_css' => '.only-css {}', + 'integration_head' => ' ', + 'integration_body' => '', + ] + ); + + $results = $scanner->scan(); + + $this->assertCount( 1, $results ); + $this->assertSame( 'Divi Custom CSS', $results[0]->name ); + $this->assertSame( 'css', $results[0]->type ); + } + + /** + * Disabling Divi's `_integrate_header_enable` toggle is reflected as `is_active = false` + * on the discovered snippet so the Unified view does not falsely report it as running. + */ + public function test_disabled_toggle_marks_snippet_inactive() { + $scanner = new Divi_Theme_Options_Scanner( + [ + 'integration_head' => '', + 'integrate_header_enable' => 'off', + ] + ); + + $results = $scanner->scan(); + + $this->assertCount( 1, $results ); + $this->assertSame( 'Divi Head Code', $results[0]->name ); + $this->assertFalse( $results[0]->is_active ); + } + + /** + * Divi treats unset toggles as 'on' (matching the checkbox `std` values in options_divi.php), + * so a populated Integration field with no stored toggle value must still report active. + */ + public function test_missing_toggle_defaults_to_active() { + $scanner = new Divi_Theme_Options_Scanner( + [ + 'integration_body' => '', + ] + ); + + $results = $scanner->scan(); + + $this->assertCount( 1, $results ); + $this->assertSame( 'Divi Body Code', $results[0]->name ); + $this->assertTrue( $results[0]->is_active ); + } + + /** + * `source_path` is deterministic (no volatile line numbers) so the snippet hash remains + * stable across repeated scans. This is what change detection in Phase 5 relies on. + */ + public function test_hash_is_stable_across_scans() { + $scanner_a = new Divi_Theme_Options_Scanner( self::FULL_FIXTURE ); + $scanner_b = new Divi_Theme_Options_Scanner( self::FULL_FIXTURE ); + + $hashes_a = wp_list_pluck( $scanner_a->scan(), 'hash' ); + $hashes_b = wp_list_pluck( $scanner_b->scan(), 'hash' ); + + sort( $hashes_a ); + sort( $hashes_b ); + + $this->assertSame( $hashes_a, $hashes_b ); + $this->assertCount( 5, array_unique( $hashes_a ) ); + } + + /** + * Editing the code of one field changes its `checksum` (so change detection fires) but not + * its `hash` (so the snippet is recognised as the same source location). + */ + public function test_checksum_changes_with_code_but_hash_stays() { + $base = new Divi_Theme_Options_Scanner( self::FULL_FIXTURE ); + + $modified_fixture = self::FULL_FIXTURE; + $modified_fixture['integration_head'] = ''; + $modified = new Divi_Theme_Options_Scanner( $modified_fixture ); + + $head_before = $this->index_by_name( $base->scan() )['Divi Head Code']; + $head_after = $this->index_by_name( $modified->scan() )['Divi Head Code']; + + $this->assertSame( $head_before->hash, $head_after->hash ); + $this->assertNotSame( $head_before->checksum, $head_after->checksum ); + } + + /** + * Without override fixtures, availability is driven by the active template. The test + * environment is not running Divi, so the scanner must report itself unavailable. + */ + public function test_is_available_false_without_divi_theme() { + $scanner = new Divi_Theme_Options_Scanner(); + + $this->assertFalse( $scanner->is_available() ); + } + + /** + * Static scanner identity surfaces correctly to the registry / REST layer. + */ + public function test_scanner_identity() { + $scanner = new Divi_Theme_Options_Scanner(); + + $this->assertSame( 'divi-theme-options', $scanner->get_id() ); + $this->assertSame( 'Divi Theme Options', $scanner->get_label() ); + $this->assertSame( 'medium', $scanner->get_risk_level() ); + $this->assertTrue( $scanner->supports_import() ); + $this->assertFalse( $scanner->supports_editing() ); + } +} From 87f690558e0c2992c26d8528450e8346536d9a3a Mon Sep 17 00:00:00 2001 From: Louis Wolmarans Date: Wed, 20 May 2026 22:36:39 +0200 Subject: [PATCH 2/4] update divi scanners --- src/composer.lock | 10 +-- .../Scanners/Divi_Theme_Options_Scanner.php | 23 ++++--- tests/phpunit/test-divi-scanner.php | 62 +++++++++++++++++++ 3 files changed, 81 insertions(+), 14 deletions(-) diff --git a/src/composer.lock b/src/composer.lock index 2f998e95..b56342ca 100644 --- a/src/composer.lock +++ b/src/composer.lock @@ -330,16 +330,16 @@ "packages-dev": [ { "name": "dealerdirect/phpcodesniffer-composer-installer", - "version": "v1.2.0", + "version": "v1.2.1", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/composer-installer.git", - "reference": "845eb62303d2ca9b289ef216356568ccc075ffd1" + "reference": "963f0c67bffde0eac41b56be71ac0e8ba132f0bd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/845eb62303d2ca9b289ef216356568ccc075ffd1", - "reference": "845eb62303d2ca9b289ef216356568ccc075ffd1", + "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/963f0c67bffde0eac41b56be71ac0e8ba132f0bd", + "reference": "963f0c67bffde0eac41b56be71ac0e8ba132f0bd", "shasum": "" }, "require": { @@ -422,7 +422,7 @@ "type": "thanks_dev" } ], - "time": "2025-11-11T04:32:07+00:00" + "time": "2026-05-06T08:26:05+00:00" }, { "name": "doctrine/instantiator", diff --git a/src/php/UnifiedSnippets/Scanners/Divi_Theme_Options_Scanner.php b/src/php/UnifiedSnippets/Scanners/Divi_Theme_Options_Scanner.php index acde900e..35f105a5 100644 --- a/src/php/UnifiedSnippets/Scanners/Divi_Theme_Options_Scanner.php +++ b/src/php/UnifiedSnippets/Scanners/Divi_Theme_Options_Scanner.php @@ -150,13 +150,16 @@ private function resolve_shortname(): string { * Read a Divi option, mirroring `et_get_option()`'s two storage modes. * * Divi may store its theme options either as a single serialised array under `et_` - * (one-row mode) or as individual options keyed `et__` (per-row mode). The - * actual mode is decided per-install by `et_options_stored_in_one_row()`. Rather than load - * that helper, we check both locations and return whichever yields a value; reads against an - * empty install harmlessly return ''. + * (one-row mode) or as individual options keyed by the full field id (per-row mode). The + * actual mode is decided per-install by `et_options_stored_in_one_row()`. Crucially, the + * field IDs in `options_divi.php` are already prefixed with the shortname (e.g. + * `divi_custom_css`, `divi_integration_head`), and that full id is what `et_get_option()` + * uses as both the array key (one-row) and the option name (per-row). Rather than load + * Divi's helper, we check both locations and return whichever yields a value; reads against + * an empty install harmlessly return ''. * - * @param string $shortname The resolved Divi shortname. - * @param string $key Option key without the `et__` prefix (e.g. `custom_css`). + * @param string $shortname The resolved Divi shortname (e.g. `divi`). + * @param string $key Option key without the shortname prefix (e.g. `custom_css`). * * @return string The stored value, or '' when unset. */ @@ -169,12 +172,14 @@ private function read_option( string $shortname, string $key ): string { return ''; } + $full_key = $shortname . '_' . $key; + $bundle = get_option( 'et_' . $shortname ); - if ( is_array( $bundle ) && isset( $bundle[ $key ] ) && '' !== (string) $bundle[ $key ] ) { - return (string) $bundle[ $key ]; + if ( is_array( $bundle ) && isset( $bundle[ $full_key ] ) && '' !== (string) $bundle[ $full_key ] ) { + return (string) $bundle[ $full_key ]; } - $value = get_option( 'et_' . $shortname . '_' . $key, '' ); + $value = get_option( $full_key, '' ); return is_scalar( $value ) ? (string) $value : ''; } diff --git a/tests/phpunit/test-divi-scanner.php b/tests/phpunit/test-divi-scanner.php index 17eb41cd..9ac8177a 100644 --- a/tests/phpunit/test-divi-scanner.php +++ b/tests/phpunit/test-divi-scanner.php @@ -213,4 +213,66 @@ public function test_scanner_identity() { $this->assertTrue( $scanner->supports_import() ); $this->assertFalse( $scanner->supports_editing() ); } + + /** + * One-row storage: Divi packs every option into `et_divi`, keyed by the full field id + * (e.g. `divi_custom_css`). The scanner must look up `_` inside that array + * rather than the bare key. Without override fixtures this exercises the real + * get_option path that production runs against. + */ + public function test_reads_real_one_row_storage() { + update_option( + 'et_divi', + [ + 'divi_custom_css' => '.real-one-row { color: green; }', + 'divi_integration_head' => '', + ] + ); + + $scanner = $this->scanner_with_forced_availability(); + $results = $this->index_by_name( $scanner->scan() ); + + try { + $this->assertArrayHasKey( 'Divi Custom CSS', $results ); + $this->assertSame( '.real-one-row { color: green; }', $results['Divi Custom CSS']->code ); + $this->assertArrayHasKey( 'Divi Head Code', $results ); + $this->assertSame( '', $results['Divi Head Code']->code ); + } finally { + delete_option( 'et_divi' ); + } + } + + /** + * Per-row storage: each field is stored as its own option named after the full field id + * (e.g. `divi_custom_css`), with no `et_` prefix. The scanner falls back to this read + * path when the `et_` bundle is missing the key. + */ + public function test_reads_real_per_row_storage() { + update_option( 'divi_integration_body', '' ); + + $scanner = $this->scanner_with_forced_availability(); + $results = $this->index_by_name( $scanner->scan() ); + + try { + $this->assertArrayHasKey( 'Divi Body Code', $results ); + $this->assertSame( + '', + $results['Divi Body Code']->code + ); + } finally { + delete_option( 'divi_integration_body' ); + } + } + + /** + * Build a scanner instance whose `is_available()` returns true regardless of the active + * theme template, so the storage-mode tests can exercise the real wp_options read path + * without needing Divi installed in the test environment. + * + * The trivial single-key override forces override-mode availability without polluting + * any of the real Divi field reads (the override key never matches a real Divi field). + */ + private function scanner_with_forced_availability(): Divi_Theme_Options_Scanner { + return new Divi_Theme_Options_Scanner( [ '__force_available__' => '' ] ); + } } From 2734c2c9c6a46a0125009ec51977dedceec465a8 Mon Sep 17 00:00:00 2001 From: Louis Wolmarans Date: Wed, 20 May 2026 22:57:08 +0200 Subject: [PATCH 3/4] update divi scanner logic --- .../Scanners/Divi_Theme_Options_Scanner.php | 35 ++++++++++--------- tests/phpunit/test-divi-scanner.php | 28 ++++++++++++--- 2 files changed, 43 insertions(+), 20 deletions(-) diff --git a/src/php/UnifiedSnippets/Scanners/Divi_Theme_Options_Scanner.php b/src/php/UnifiedSnippets/Scanners/Divi_Theme_Options_Scanner.php index 35f105a5..b8d31e52 100644 --- a/src/php/UnifiedSnippets/Scanners/Divi_Theme_Options_Scanner.php +++ b/src/php/UnifiedSnippets/Scanners/Divi_Theme_Options_Scanner.php @@ -187,23 +187,25 @@ private function read_option( string $shortname, string $key ): string { /** * Read one of Divi's `_integrate_*_enable` toggles. * - * Divi treats these as 'on' by default when unset, matching the checkbox `std` values in - * options_divi.php. We do the same so a freshly populated Integration field is reported as - * active rather than silently inactive. + * Mirrors Divi's runtime check (see `integration_head()` in custom_functions.php), which + * emits code only when the toggle value is strictly equal to `'on'`. Other states the + * scanner may encounter: + * + * - Stored as `'false'` (string) when the checkbox was explicitly unchecked at save. + * Divi's save handler writes the literal string `'false'` for unchecked checkboxes + * (see `epanel_save_data()`). + * - Missing entirely on a never-saved install. `et_get_option()` returns boolean false + * in that case, and `false === 'on'` is also false, so Divi does NOT emit. We match + * that here — the checkbox `std => 'on'` in options_divi.php controls the admin UI's + * default-checked state, not the runtime default. * * @param string $shortname The resolved Divi shortname. - * @param string $key Toggle key without the `et__` prefix. + * @param string $key Toggle key without the `_` prefix. * * @return bool */ private function read_toggle( string $shortname, string $key ): bool { - $value = $this->read_option( $shortname, $key ); - - if ( '' === $value ) { - return true; - } - - return 'on' === $value; + return 'on' === $this->read_option( $shortname, $key ); } /** @@ -265,7 +267,7 @@ private function scan_integration_head( string $shortname, string $source_name ) 'source_name' => $source_name, 'source_path' => 'divi://theme-options/integration_head', 'is_active' => $this->read_toggle( $shortname, 'integrate_header_enable' ), - 'import_notes' => __( 'Imports into the head-content scope (rendered on wp_head).', 'code-snippets' ), + 'import_notes' => __( 'Divi renders this on wp_head at priority 12. Importing as head-content runs it on wp_head at priority 10, so the imported snippet executes slightly earlier than Divi did. Usually harmless, but may matter for order-sensitive scripts.', 'code-snippets' ), ] ); } @@ -275,9 +277,10 @@ private function scan_integration_head( string $shortname, string $source_name ) * * Despite Divi's UI labelling this "body" code, the theme actually emits the content on * `wp_footer` at priority 12 (see Divi/epanel/custom_functions.php). Importing into - * Code Snippets' `footer-content` scope (also wp_footer, priority 20) reproduces the - * runtime behaviour. The priority delta is surfaced in `import_notes` for users with - * order-sensitive scripts. + * Code Snippets' `footer-content` scope (also wp_footer, default priority 10 per + * `Evaluate_Content::init()`) reproduces the runtime behaviour, just executed slightly + * earlier. The priority delta is surfaced in `import_notes` for users with order-sensitive + * scripts. * * @param string $shortname The Divi option shortname. * @param string $source_name Human-readable theme name. @@ -303,7 +306,7 @@ private function scan_integration_body( string $shortname, string $source_name ) 'source_name' => $source_name, 'source_path' => 'divi://theme-options/integration_body', 'is_active' => $this->read_toggle( $shortname, 'integrate_body_enable' ), - 'import_notes' => __( 'Divi renders this on wp_footer at priority 12. Importing as footer-content runs it on wp_footer at priority 20, which may matter for order-sensitive scripts.', 'code-snippets' ), + 'import_notes' => __( 'Divi renders this on wp_footer at priority 12. Importing as footer-content runs it on wp_footer at priority 10, so the imported snippet executes slightly earlier than Divi did. Usually harmless, but may matter for order-sensitive scripts.', 'code-snippets' ), ] ); } diff --git a/tests/phpunit/test-divi-scanner.php b/tests/phpunit/test-divi-scanner.php index 9ac8177a..14c57898 100644 --- a/tests/phpunit/test-divi-scanner.php +++ b/tests/phpunit/test-divi-scanner.php @@ -138,10 +138,11 @@ public function test_disabled_toggle_marks_snippet_inactive() { } /** - * Divi treats unset toggles as 'on' (matching the checkbox `std` values in options_divi.php), - * so a populated Integration field with no stored toggle value must still report active. + * Divi's runtime check uses strict equality against `'on'`, so a populated Integration field + * with no stored toggle value (which `et_get_option()` returns as boolean false) is NOT + * actually emitted by Divi. The scanner must mirror that. */ - public function test_missing_toggle_defaults_to_active() { + public function test_missing_toggle_reports_inactive() { $scanner = new Divi_Theme_Options_Scanner( [ 'integration_body' => '', @@ -152,7 +153,26 @@ public function test_missing_toggle_defaults_to_active() { $this->assertCount( 1, $results ); $this->assertSame( 'Divi Body Code', $results[0]->name ); - $this->assertTrue( $results[0]->is_active ); + $this->assertFalse( $results[0]->is_active ); + } + + /** + * Divi's save handler writes the literal string `'false'` when a checkbox is unchecked at + * save time. That is not strictly equal to `'on'`, so the snippet must report inactive. + */ + public function test_literal_false_toggle_reports_inactive() { + $scanner = new Divi_Theme_Options_Scanner( + [ + 'integration_head' => '', + 'integrate_header_enable' => 'false', + ] + ); + + $results = $scanner->scan(); + + $this->assertCount( 1, $results ); + $this->assertSame( 'Divi Head Code', $results[0]->name ); + $this->assertFalse( $results[0]->is_active ); } /** From c8bacc3737e7031ef3b1434f965391ba1277a247 Mon Sep 17 00:00:00 2001 From: Louis Wolmarans Date: Wed, 20 May 2026 23:44:44 +0200 Subject: [PATCH 4/4] improve unit tests --- .../Scanners/Divi_Theme_Options_Scanner.php | 32 +-- tests/phpunit/test-divi-scanner.php | 249 ++++++++++++------ 2 files changed, 164 insertions(+), 117 deletions(-) diff --git a/src/php/UnifiedSnippets/Scanners/Divi_Theme_Options_Scanner.php b/src/php/UnifiedSnippets/Scanners/Divi_Theme_Options_Scanner.php index b8d31e52..864133f8 100644 --- a/src/php/UnifiedSnippets/Scanners/Divi_Theme_Options_Scanner.php +++ b/src/php/UnifiedSnippets/Scanners/Divi_Theme_Options_Scanner.php @@ -33,31 +33,11 @@ class Divi_Theme_Options_Scanner extends Scanner_Base { private const SUPPORTED_TEMPLATES = [ 'Divi', 'Extra' ]; /** - * Default shortname used when the active template is not one of the supported themes - * (only relevant in override-mode for tests). + * Fallback shortname when the active template is not Extra. Production only reaches this + * branch when running against Divi itself (the parent theme). */ private const DEFAULT_SHORTNAME = 'divi'; - /** - * Optional in-memory overrides keyed by Divi option name (without the `et__` prefix). - * - * Used by tests to inject fixture values without touching wp_options. When non-empty, the - * scanner also treats itself as available regardless of the active template so test fixtures - * can drive end-to-end behaviour. - * - * @var array - */ - private array $option_overrides; - - /** - * Class constructor. - * - * @param array $option_overrides Optional option-value overrides for testing. - */ - public function __construct( array $option_overrides = [] ) { - $this->option_overrides = $option_overrides; - } - /** * {@inheritDoc} */ @@ -80,10 +60,6 @@ public function get_label(): string { * dormant and the namespaced option keys belong to an inactive theme. */ public function is_available(): bool { - if ( $this->option_overrides ) { - return true; - } - if ( ! function_exists( 'get_template' ) ) { return false; } @@ -164,10 +140,6 @@ private function resolve_shortname(): string { * @return string The stored value, or '' when unset. */ private function read_option( string $shortname, string $key ): string { - if ( array_key_exists( $key, $this->option_overrides ) ) { - return (string) $this->option_overrides[ $key ]; - } - if ( ! function_exists( 'get_option' ) ) { return ''; } diff --git a/tests/phpunit/test-divi-scanner.php b/tests/phpunit/test-divi-scanner.php index 14c57898..a90afb31 100644 --- a/tests/phpunit/test-divi-scanner.php +++ b/tests/phpunit/test-divi-scanner.php @@ -7,30 +7,71 @@ /** * Tests for the Tier 3 Divi Theme Options scanner. * - * The scanner is constructed with option-value overrides to avoid touching wp_options or - * needing Divi installed in the test environment. Override mode also forces is_available() - * to true so end-to-end behaviour can be exercised without activating the theme. + * The scanner has no test-only seams: all fixtures are written through real WordPress APIs + * (`update_option()` for wp_options, and the `pre_option_template` / `pre_option_stylesheet` + * filters for `get_template()`). Each test that seeds data is responsible for cleaning up, + * and the filter callbacks are removed in tearDown. * * @group unified-snippets */ class Divi_Scanner_Test extends TestCase { /** - * Populated fixture covering all five Divi Theme Options fields. + * Closures registered against `pre_option_*` filters during a test, captured so they can + * be removed in tearDown. * - * @var array + * @var array */ - private const FULL_FIXTURE = [ - 'custom_css' => '.divi-test { color: red; }', - 'integration_head' => '', - 'integration_body' => '', - 'integration_single_top' => '
Top
', - 'integration_single_bottom' => '
Bottom
', - 'integrate_header_enable' => 'on', - 'integrate_body_enable' => 'on', - 'integrate_singletop_enable' => 'on', - 'integrate_singlebottom_enable' => 'on', - ]; + private array $registered_filters = []; + + /** + * Tear down per-test fixtures: remove any `pre_option_*` filters this test registered, and + * delete the wp_options rows the per-storage tests seed. + */ + public function tear_down() { + foreach ( $this->registered_filters as $entry ) { + remove_filter( $entry['hook'], $entry['callback'] ); + } + + $this->registered_filters = []; + + delete_option( 'et_divi' ); + delete_option( 'divi_custom_css' ); + delete_option( 'divi_integration_head' ); + delete_option( 'divi_integration_body' ); + delete_option( 'divi_integration_single_top' ); + delete_option( 'divi_integration_single_bottom' ); + delete_option( 'divi_integrate_header_enable' ); + delete_option( 'divi_integrate_body_enable' ); + delete_option( 'divi_integrate_singletop_enable' ); + delete_option( 'divi_integrate_singlebottom_enable' ); + + parent::tear_down(); + } + + /** + * Force `get_template()` (and its companion `get_stylesheet()`) to report a chosen theme + * slug for the duration of the test, without activating that theme on disk. Uses WordPress's + * own `pre_option_*` short-circuit filters so we don't poke at scanner internals. + * + * @param string $template Slug to return from get_template(), e.g. 'Divi' or 'twentytwentyfour'. + * + * @return void + */ + private function pretend_active_template( string $template ): void { + $callback = static function () use ( $template ) { + return $template; + }; + + foreach ( [ 'pre_option_template', 'pre_option_stylesheet' ] as $hook ) { + add_filter( $hook, $callback ); + + $this->registered_filters[] = [ + 'hook' => $hook, + 'callback' => $callback, + ]; + } + } /** * Index a scan result by snippet name for easier per-field assertions. @@ -50,11 +91,38 @@ private function index_by_name( array $results ): array { } /** - * A populated fixture produces one Discovered_Snippet per Divi field with the expected - * type, source metadata, and active flags. + * Write a fully populated `et_divi` bundle (one-row storage mode) covering every field + * the scanner knows about, with all enable toggles set to `'on'`. + * + * @return void + */ + private function seed_full_divi_bundle(): void { + update_option( + 'et_divi', + [ + 'divi_custom_css' => '.divi-test { color: red; }', + 'divi_integration_head' => '', + 'divi_integration_body' => '', + 'divi_integration_single_top' => '
Top
', + 'divi_integration_single_bottom' => '
Bottom
', + 'divi_integrate_header_enable' => 'on', + 'divi_integrate_body_enable' => 'on', + 'divi_integrate_singletop_enable' => 'on', + 'divi_integrate_singlebottom_enable' => 'on', + ] + ); + } + + /** + * A fully populated Divi install produces one Discovered_Snippet per field with the + * expected type, source metadata, and active flags. Uses one-row storage mode (the modern + * Divi default). */ public function test_scan_returns_one_snippet_per_populated_field() { - $scanner = new Divi_Theme_Options_Scanner( self::FULL_FIXTURE ); + $this->pretend_active_template( 'Divi' ); + $this->seed_full_divi_bundle(); + + $scanner = new Divi_Theme_Options_Scanner(); $this->assertTrue( $scanner->is_available() ); @@ -103,15 +171,18 @@ public function test_scan_returns_one_snippet_per_populated_field() { * surfaces the fields the user has actually filled in. */ public function test_scan_skips_empty_fields() { - $scanner = new Divi_Theme_Options_Scanner( + $this->pretend_active_template( 'Divi' ); + + update_option( + 'et_divi', [ - 'custom_css' => '.only-css {}', - 'integration_head' => ' ', - 'integration_body' => '', + 'divi_custom_css' => '.only-css {}', + 'divi_integration_head' => ' ', + 'divi_integration_body' => '', ] ); - $results = $scanner->scan(); + $results = ( new Divi_Theme_Options_Scanner() )->scan(); $this->assertCount( 1, $results ); $this->assertSame( 'Divi Custom CSS', $results[0]->name ); @@ -119,18 +190,22 @@ public function test_scan_skips_empty_fields() { } /** - * Disabling Divi's `_integrate_header_enable` toggle is reflected as `is_active = false` - * on the discovered snippet so the Unified view does not falsely report it as running. + * Disabling Divi's `_integrate_header_enable` toggle (stored as `'off'`) is reflected as + * `is_active = false` on the discovered snippet so the Unified view does not falsely + * report it as running. */ public function test_disabled_toggle_marks_snippet_inactive() { - $scanner = new Divi_Theme_Options_Scanner( + $this->pretend_active_template( 'Divi' ); + + update_option( + 'et_divi', [ - 'integration_head' => '', - 'integrate_header_enable' => 'off', + 'divi_integration_head' => '', + 'divi_integrate_header_enable' => 'off', ] ); - $results = $scanner->scan(); + $results = ( new Divi_Theme_Options_Scanner() )->scan(); $this->assertCount( 1, $results ); $this->assertSame( 'Divi Head Code', $results[0]->name ); @@ -143,13 +218,16 @@ public function test_disabled_toggle_marks_snippet_inactive() { * actually emitted by Divi. The scanner must mirror that. */ public function test_missing_toggle_reports_inactive() { - $scanner = new Divi_Theme_Options_Scanner( + $this->pretend_active_template( 'Divi' ); + + update_option( + 'et_divi', [ - 'integration_body' => '', + 'divi_integration_body' => '', ] ); - $results = $scanner->scan(); + $results = ( new Divi_Theme_Options_Scanner() )->scan(); $this->assertCount( 1, $results ); $this->assertSame( 'Divi Body Code', $results[0]->name ); @@ -161,14 +239,17 @@ public function test_missing_toggle_reports_inactive() { * save time. That is not strictly equal to `'on'`, so the snippet must report inactive. */ public function test_literal_false_toggle_reports_inactive() { - $scanner = new Divi_Theme_Options_Scanner( + $this->pretend_active_template( 'Divi' ); + + update_option( + 'et_divi', [ - 'integration_head' => '', - 'integrate_header_enable' => 'false', + 'divi_integration_head' => '', + 'divi_integrate_header_enable' => 'false', ] ); - $results = $scanner->scan(); + $results = ( new Divi_Theme_Options_Scanner() )->scan(); $this->assertCount( 1, $results ); $this->assertSame( 'Divi Head Code', $results[0]->name ); @@ -180,11 +261,11 @@ public function test_literal_false_toggle_reports_inactive() { * stable across repeated scans. This is what change detection in Phase 5 relies on. */ public function test_hash_is_stable_across_scans() { - $scanner_a = new Divi_Theme_Options_Scanner( self::FULL_FIXTURE ); - $scanner_b = new Divi_Theme_Options_Scanner( self::FULL_FIXTURE ); + $this->pretend_active_template( 'Divi' ); + $this->seed_full_divi_bundle(); - $hashes_a = wp_list_pluck( $scanner_a->scan(), 'hash' ); - $hashes_b = wp_list_pluck( $scanner_b->scan(), 'hash' ); + $hashes_a = wp_list_pluck( ( new Divi_Theme_Options_Scanner() )->scan(), 'hash' ); + $hashes_b = wp_list_pluck( ( new Divi_Theme_Options_Scanner() )->scan(), 'hash' ); sort( $hashes_a ); sort( $hashes_b ); @@ -198,27 +279,40 @@ public function test_hash_is_stable_across_scans() { * its `hash` (so the snippet is recognised as the same source location). */ public function test_checksum_changes_with_code_but_hash_stays() { - $base = new Divi_Theme_Options_Scanner( self::FULL_FIXTURE ); + $this->pretend_active_template( 'Divi' ); + $this->seed_full_divi_bundle(); + + $head_before = $this->index_by_name( ( new Divi_Theme_Options_Scanner() )->scan() )['Divi Head Code']; - $modified_fixture = self::FULL_FIXTURE; - $modified_fixture['integration_head'] = ''; - $modified = new Divi_Theme_Options_Scanner( $modified_fixture ); + $bundle = get_option( 'et_divi' ); + $bundle['divi_integration_head'] = ''; + update_option( 'et_divi', $bundle ); - $head_before = $this->index_by_name( $base->scan() )['Divi Head Code']; - $head_after = $this->index_by_name( $modified->scan() )['Divi Head Code']; + $head_after = $this->index_by_name( ( new Divi_Theme_Options_Scanner() )->scan() )['Divi Head Code']; $this->assertSame( $head_before->hash, $head_after->hash ); $this->assertNotSame( $head_before->checksum, $head_after->checksum ); } /** - * Without override fixtures, availability is driven by the active template. The test - * environment is not running Divi, so the scanner must report itself unavailable. + * Without Divi (or Extra) as the active template, the scanner reports itself unavailable + * even if the wp_options rows still exist. Divi's hooks are not running, so the code is + * dormant and should not be surfaced. */ public function test_is_available_false_without_divi_theme() { - $scanner = new Divi_Theme_Options_Scanner(); + $this->pretend_active_template( 'twentytwentyfour' ); - $this->assertFalse( $scanner->is_available() ); + $this->assertFalse( ( new Divi_Theme_Options_Scanner() )->is_available() ); + } + + /** + * The sibling theme Extra shares Divi's option storage layout under a different shortname, + * so the scanner reports itself available when Extra is the active template. + */ + public function test_is_available_true_for_extra_theme() { + $this->pretend_active_template( 'Extra' ); + + $this->assertTrue( ( new Divi_Theme_Options_Scanner() )->is_available() ); } /** @@ -237,10 +331,11 @@ public function test_scanner_identity() { /** * One-row storage: Divi packs every option into `et_divi`, keyed by the full field id * (e.g. `divi_custom_css`). The scanner must look up `_` inside that array - * rather than the bare key. Without override fixtures this exercises the real - * get_option path that production runs against. + * rather than the bare key. */ public function test_reads_real_one_row_storage() { + $this->pretend_active_template( 'Divi' ); + update_option( 'et_divi', [ @@ -249,50 +344,30 @@ public function test_reads_real_one_row_storage() { ] ); - $scanner = $this->scanner_with_forced_availability(); - $results = $this->index_by_name( $scanner->scan() ); + $results = $this->index_by_name( ( new Divi_Theme_Options_Scanner() )->scan() ); - try { - $this->assertArrayHasKey( 'Divi Custom CSS', $results ); - $this->assertSame( '.real-one-row { color: green; }', $results['Divi Custom CSS']->code ); - $this->assertArrayHasKey( 'Divi Head Code', $results ); - $this->assertSame( '', $results['Divi Head Code']->code ); - } finally { - delete_option( 'et_divi' ); - } + $this->assertArrayHasKey( 'Divi Custom CSS', $results ); + $this->assertSame( '.real-one-row { color: green; }', $results['Divi Custom CSS']->code ); + $this->assertArrayHasKey( 'Divi Head Code', $results ); + $this->assertSame( '', $results['Divi Head Code']->code ); } /** * Per-row storage: each field is stored as its own option named after the full field id - * (e.g. `divi_custom_css`), with no `et_` prefix. The scanner falls back to this read + * (e.g. `divi_integration_body`), with no `et_` prefix. The scanner falls back to this read * path when the `et_` bundle is missing the key. */ public function test_reads_real_per_row_storage() { + $this->pretend_active_template( 'Divi' ); + update_option( 'divi_integration_body', '' ); - $scanner = $this->scanner_with_forced_availability(); - $results = $this->index_by_name( $scanner->scan() ); - - try { - $this->assertArrayHasKey( 'Divi Body Code', $results ); - $this->assertSame( - '', - $results['Divi Body Code']->code - ); - } finally { - delete_option( 'divi_integration_body' ); - } - } + $results = $this->index_by_name( ( new Divi_Theme_Options_Scanner() )->scan() ); - /** - * Build a scanner instance whose `is_available()` returns true regardless of the active - * theme template, so the storage-mode tests can exercise the real wp_options read path - * without needing Divi installed in the test environment. - * - * The trivial single-key override forces override-mode availability without polluting - * any of the real Divi field reads (the override key never matches a real Divi field). - */ - private function scanner_with_forced_availability(): Divi_Theme_Options_Scanner { - return new Divi_Theme_Options_Scanner( [ '__force_available__' => '' ] ); + $this->assertArrayHasKey( 'Divi Body Code', $results ); + $this->assertSame( + '', + $results['Divi Body Code']->code + ); } }