From 1ac4288ad91eacfae03b658dead212467bf4af64 Mon Sep 17 00:00:00 2001 From: lucadobrescu <252785083+lucadobrescu@users.noreply.github.com> Date: Fri, 31 Jul 2026 10:09:58 +0300 Subject: [PATCH 01/10] fix: keep usage logger alive when chart settings meta is not an array A published chart whose visualizer-settings meta resolves to a string crashed Visualizer_Module_Setup::getUsage() with a PHP 8 TypeError at the unchecked array_key_exists() call, aborting the whole Themeisle SDK usage collection request. Guard the settings read, harden the sibling pro-permissions meta read the same way, and drop the dangling visualizer_logger_data registration in Visualizer_Module_Admin, which points to a method that class never had and fatals the same filter once the first crash is fixed. Fixes #1359 Co-Authored-By: Claude Fable 5 --- classes/Visualizer/Module/Admin.php | 1 - classes/Visualizer/Module/Setup.php | 4 +- .../mu-plugins/plant-chart-settings.php | 16 ++++ tests/e2e/specs/usage-logger.spec.js | 61 +++++++++++++++ tests/test-usage-logger.php | 76 +++++++++++++++++++ 5 files changed, 155 insertions(+), 3 deletions(-) create mode 100644 tests/e2e/specs/usage-logger.spec.js create mode 100644 tests/test-usage-logger.php diff --git a/classes/Visualizer/Module/Admin.php b/classes/Visualizer/Module/Admin.php index e003f2e60..e7ef36e5f 100644 --- a/classes/Visualizer/Module/Admin.php +++ b/classes/Visualizer/Module/Admin.php @@ -81,7 +81,6 @@ public function __construct( Visualizer_Plugin $plugin ) { $this->_addFilter( 'media_view_strings', 'setupMediaViewStrings' ); $this->_addFilter( 'plugin_action_links', 'getPluginActionLinks', 10, 2 ); $this->_addFilter( 'plugin_row_meta', 'getPluginMetaLinks', 10, 2 ); - $this->_addFilter( 'visualizer_logger_data', 'getLoggerData' ); $this->_addFilter( 'visualizer_feedback_review_trigger', 'feedbackReviewTrigger' ); $this->_addFilter( 'themeisle_sdk_blackfriday_data', 'add_black_friday_data' ); diff --git a/classes/Visualizer/Module/Setup.php b/classes/Visualizer/Module/Setup.php index f0cc81f47..f2ca70769 100644 --- a/classes/Visualizer/Module/Setup.php +++ b/classes/Visualizer/Module/Setup.php @@ -113,7 +113,7 @@ public function getUsage( $data, $meta_keys = array() ) { $lib = get_post_meta( $chart_id, Visualizer_Plugin::CF_CHART_LIBRARY, true ); $charts['library'][ $lib ] = isset( $charts['library'][ $lib ] ) ? $charts['library'][ $lib ] + 1 : 1; $settings = get_post_meta( $chart_id, Visualizer_Plugin::CF_SETTINGS, true ); - if ( array_key_exists( 'manual', $settings ) && ! empty( $settings['manual'] ) ) { + if ( is_array( $settings ) && ! empty( $settings['manual'] ) ) { $charts['manual_config'] = $charts['manual_config'] + 1; } @@ -124,7 +124,7 @@ public function getUsage( $data, $meta_keys = array() ) { if ( Visualizer_Module::is_pro() ) { $permissions = get_post_meta( $chart_id, Visualizer_Pro::CF_PERMISSIONS, true ); - if ( empty( $permissions ) ) { + if ( ! is_array( $permissions ) || empty( $permissions['permissions'] ) ) { continue; } $permissions = $permissions['permissions']; diff --git a/tests/e2e/config/mu-plugins/plant-chart-settings.php b/tests/e2e/config/mu-plugins/plant-chart-settings.php index 5222e9de8..5a07d4a00 100644 --- a/tests/e2e/config/mu-plugins/plant-chart-settings.php +++ b/tests/e2e/config/mu-plugins/plant-chart-settings.php @@ -38,5 +38,21 @@ function () { }, ) ); + + // Runs the SDK usage logger on demand, so specs can verify it + // tolerates whatever chart meta they planted (issue #1359). + register_rest_route( + 'visualizer-e2e/v1', + '/usage', + array( + 'methods' => 'GET', + 'permission_callback' => function () { + return current_user_can( 'manage_options' ); + }, + 'callback' => function () { + return apply_filters( 'visualizer_logger_data', array() ); + }, + ) + ); } ); diff --git a/tests/e2e/specs/usage-logger.spec.js b/tests/e2e/specs/usage-logger.spec.js new file mode 100644 index 000000000..9178d2e3f --- /dev/null +++ b/tests/e2e/specs/usage-logger.spec.js @@ -0,0 +1,61 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +/** + * Regression tests for https://github.com/Codeinwp/visualizer/issues/1359 + * + * A published chart whose `visualizer-settings` meta is a string (instead of + * the sanitized settings array) crashed `Visualizer_Module_Setup::getUsage()` + * with a PHP 8 TypeError, aborting the whole SDK usage collection request. + * The logger must tolerate such charts and still report the others. + */ +test.describe( 'Usage logger', () => { + let corruptedId; + let manualId; + + test.beforeAll( async ( { requestUtils } ) => { + // A chart whose settings meta is a corrupted string value. + const corrupted = await requestUtils.rest( { + method: 'POST', + path: '/wp/v2/visualizer', + data: { title: 'Corrupted settings chart', status: 'publish' }, + } ); + corruptedId = corrupted.id; + await requestUtils.rest( { + method: 'POST', + path: `/visualizer-e2e/v1/chart-settings/${ corruptedId }`, + data: { settings: 'corrupted string settings' }, + } ); + + // A healthy chart with a manual configuration, which must still be counted. + const manual = await requestUtils.rest( { + method: 'POST', + path: '/wp/v2/visualizer', + data: { title: 'Manual config chart', status: 'publish' }, + } ); + manualId = manual.id; + await requestUtils.rest( { + method: 'POST', + path: `/visualizer-e2e/v1/chart-settings/${ manualId }`, + data: { settings: { manual: '{"colors": ["#000"]}' } }, + } ); + } ); + + test.afterAll( async ( { requestUtils } ) => { + for ( const id of [ corruptedId, manualId ] ) { + if ( id ) { + await requestUtils.rest( { method: 'DELETE', path: `/wp/v2/visualizer/${ id }`, params: { force: true } } ); + } + } + } ); + + test( 'survives a chart whose settings meta is a string', async ( { requestUtils } ) => { + // Before the fix this request died with a TypeError (HTTP 500). + const usage = await requestUtils.rest( { method: 'GET', path: '/visualizer-e2e/v1/usage' } ); + + expect( usage.manual_config ).toBe( 1 ); + expect( Object.values( usage.types ).reduce( ( a, b ) => a + b, 0 ) ).toBe( 2 ); + } ); +} ); diff --git a/tests/test-usage-logger.php b/tests/test-usage-logger.php new file mode 100644 index 000000000..1b6e9a2b5 --- /dev/null +++ b/tests/test-usage-logger.php @@ -0,0 +1,76 @@ +post->create( + array( + 'post_type' => Visualizer_Plugin::CPT_VISUALIZER, + 'post_status' => 'publish', + 'post_content' => wp_slash( serialize( array( array( 'Label' ), array( 'Value' ) ) ) ), + ) + ); + update_post_meta( $chart_id, Visualizer_Plugin::CF_CHART_TYPE, 'line' ); + update_post_meta( $chart_id, Visualizer_Plugin::CF_SETTINGS, $settings ); + return $chart_id; + } + + /** + * A chart whose settings meta is a string must not abort usage collection. + */ + public function test_string_settings_meta_does_not_crash_logger() { + $this->create_chart( 'corrupted string settings' ); + + $usage = apply_filters( 'visualizer_logger_data', array() ); + + $this->assertIsArray( $usage ); + $this->assertSame( 0, $usage['manual_config'] ); + } + + /** + * A chart with no settings meta at all must not abort usage collection. + */ + public function test_missing_settings_meta_does_not_crash_logger() { + $chart_id = $this->create_chart( array() ); + delete_post_meta( $chart_id, Visualizer_Plugin::CF_SETTINGS ); + + $usage = apply_filters( 'visualizer_logger_data', array() ); + + $this->assertIsArray( $usage ); + $this->assertSame( 0, $usage['manual_config'] ); + } + + /** + * Valid array settings still count manual configurations. + */ + public function test_manual_config_still_counted_for_array_settings() { + $this->create_chart( array( 'manual' => '{"colors": ["#000"]}' ) ); + $this->create_chart( 'corrupted string settings' ); + + $usage = apply_filters( 'visualizer_logger_data', array() ); + + $this->assertSame( 1, $usage['manual_config'] ); + $this->assertSame( 2, $usage['types']['line'] ); + } +} From a4de84188a6f866d7c26a8b7c4a0bdb28df13371 Mon Sep 17 00:00:00 2001 From: lucadobrescu <252785083+lucadobrescu@users.noreply.github.com> Date: Fri, 31 Jul 2026 10:32:21 +0300 Subject: [PATCH 02/10] fix: guard per-key permission counts against malformed pro meta MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copilot review follow-ups: a string where an array-valued permission is expected (e.g. permissions.edit-specific) still crashed the usage logger via count() on PHP 8 — guard it and cover the pro branch through the visualizer_is_pro filter with a stubbed Visualizer_Pro. Also clear leftover charts before the e2e usage assertions, matching the other chart-counting specs. Co-Authored-By: Claude Fable 5 --- classes/Visualizer/Module/Setup.php | 2 +- tests/e2e/specs/usage-logger.spec.js | 8 ++++++++ tests/test-usage-logger.php | 27 +++++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/classes/Visualizer/Module/Setup.php b/classes/Visualizer/Module/Setup.php index f2ca70769..52cbc58d1 100644 --- a/classes/Visualizer/Module/Setup.php +++ b/classes/Visualizer/Module/Setup.php @@ -132,7 +132,7 @@ public function getUsage( $data, $meta_keys = array() ) { foreach ( $default_perms as $key => $val ) { if ( ! is_array( $val ) && ! is_null( $val ) && isset( $permissions[ $key ] ) && $permissions[ $key ] !== $val ) { $customized = true; - } elseif ( is_array( $val ) && ! is_null( $val ) && isset( $permissions[ $key ] ) && count( $permissions[ $key ] ) !== count( $val ) ) { + } elseif ( is_array( $val ) && ! is_null( $val ) && isset( $permissions[ $key ] ) && is_array( $permissions[ $key ] ) && count( $permissions[ $key ] ) !== count( $val ) ) { $customized = true; } } diff --git a/tests/e2e/specs/usage-logger.spec.js b/tests/e2e/specs/usage-logger.spec.js index 9178d2e3f..d502e15b3 100644 --- a/tests/e2e/specs/usage-logger.spec.js +++ b/tests/e2e/specs/usage-logger.spec.js @@ -3,6 +3,11 @@ */ const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); +/** + * Internal dependencies + */ +const { deleteAllCharts } = require( '../utils/common' ); + /** * Regression tests for https://github.com/Codeinwp/visualizer/issues/1359 * @@ -16,6 +21,9 @@ test.describe( 'Usage logger', () => { let manualId; test.beforeAll( async ( { requestUtils } ) => { + // The assertions below count charts, so start from a clean library. + await deleteAllCharts( requestUtils ); + // A chart whose settings meta is a corrupted string value. const corrupted = await requestUtils.rest( { method: 'POST', diff --git a/tests/test-usage-logger.php b/tests/test-usage-logger.php index 1b6e9a2b5..42ee72c71 100644 --- a/tests/test-usage-logger.php +++ b/tests/test-usage-logger.php @@ -61,6 +61,33 @@ public function test_missing_settings_meta_does_not_crash_logger() { $this->assertSame( 0, $usage['manual_config'] ); } + /** + * On pro, a permission entry that should be an array but is a string + * must not abort usage collection with a count() TypeError. + */ + public function test_malformed_permissions_meta_does_not_crash_logger() { + // The stub stays defined for the rest of the PHPUnit process. That only + // affects code gating on class_exists( 'Visualizer_Pro' ) — the legacy + // license fallback in proFeaturesEnabled() — which no test exercises. + if ( ! class_exists( 'Visualizer_Pro' ) ) { + eval( 'class Visualizer_Pro { const CF_PERMISSIONS = "visualizer-permissions"; }' ); + } + + $chart_id = $this->create_chart( array() ); + update_post_meta( + $chart_id, + Visualizer_Pro::CF_PERMISSIONS, + array( 'permissions' => array( 'edit-specific' => 'administrator' ) ) + ); + + add_filter( 'visualizer_is_pro', '__return_true' ); + $usage = apply_filters( 'visualizer_logger_data', array() ); + remove_filter( 'visualizer_is_pro', '__return_true' ); + + $this->assertIsArray( $usage ); + $this->assertSame( 0, $usage['permissions'] ); + } + /** * Valid array settings still count manual configurations. */ From 54aeb7832ab57eaf913b0b3e8218be5abebdd306 Mon Sep 17 00:00:00 2001 From: lucadobrescu <252785083+lucadobrescu@users.noreply.github.com> Date: Mon, 3 Aug 2026 10:05:53 +0300 Subject: [PATCH 03/10] fix: skip pro charts whose nested permissions meta is not an array An object stored in permissions['permissions'] passed the array guard and then fatalled on the array offset read, aborting usage collection. Co-Authored-By: Claude Opus 5 (1M context) --- classes/Visualizer/Module/Setup.php | 2 +- tests/test-usage-logger.php | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/classes/Visualizer/Module/Setup.php b/classes/Visualizer/Module/Setup.php index 52cbc58d1..2ef61c60c 100644 --- a/classes/Visualizer/Module/Setup.php +++ b/classes/Visualizer/Module/Setup.php @@ -124,7 +124,7 @@ public function getUsage( $data, $meta_keys = array() ) { if ( Visualizer_Module::is_pro() ) { $permissions = get_post_meta( $chart_id, Visualizer_Pro::CF_PERMISSIONS, true ); - if ( ! is_array( $permissions ) || empty( $permissions['permissions'] ) ) { + if ( ! is_array( $permissions ) || empty( $permissions['permissions'] ) || ! is_array( $permissions['permissions'] ) ) { continue; } $permissions = $permissions['permissions']; diff --git a/tests/test-usage-logger.php b/tests/test-usage-logger.php index 42ee72c71..2abf25a92 100644 --- a/tests/test-usage-logger.php +++ b/tests/test-usage-logger.php @@ -62,8 +62,9 @@ public function test_missing_settings_meta_does_not_crash_logger() { } /** - * On pro, a permission entry that should be an array but is a string - * must not abort usage collection with a count() TypeError. + * On pro, permission meta that is not shaped like a map of arrays must not + * abort usage collection — a string entry fatals on count(), and a nested + * object fatals on the array offset read. */ public function test_malformed_permissions_meta_does_not_crash_logger() { // The stub stays defined for the rest of the PHPUnit process. That only @@ -80,6 +81,9 @@ public function test_malformed_permissions_meta_does_not_crash_logger() { array( 'permissions' => array( 'edit-specific' => 'administrator' ) ) ); + $object_chart_id = $this->create_chart( array() ); + update_post_meta( $object_chart_id, Visualizer_Pro::CF_PERMISSIONS, array( 'permissions' => new stdClass() ) ); + add_filter( 'visualizer_is_pro', '__return_true' ); $usage = apply_filters( 'visualizer_logger_data', array() ); remove_filter( 'visualizer_is_pro', '__return_true' ); From 9c6cce5056adfa7757b555f3a0f6c254cb298828 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Aug 2026 16:12:45 +0000 Subject: [PATCH 04/10] chore(deps): bump codeinwp/themeisle-sdk from 3.3.57 to 3.3.58 Bumps [codeinwp/themeisle-sdk](https://github.com/Codeinwp/themeisle-sdk) from 3.3.57 to 3.3.58. - [Release notes](https://github.com/Codeinwp/themeisle-sdk/releases) - [Changelog](https://github.com/Codeinwp/themeisle-sdk/blob/v3.3.58/CHANGELOG.md) - [Commits](https://github.com/Codeinwp/themeisle-sdk/compare/v3.3.57...v3.3.58) --- updated-dependencies: - dependency-name: codeinwp/themeisle-sdk dependency-version: 3.3.58 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- composer.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/composer.lock b/composer.lock index df2b7b77d..81e48703f 100644 --- a/composer.lock +++ b/composer.lock @@ -8,16 +8,16 @@ "packages": [ { "name": "codeinwp/themeisle-sdk", - "version": "3.3.57", + "version": "3.3.58", "source": { "type": "git", "url": "https://github.com/Codeinwp/themeisle-sdk.git", - "reference": "3c761b0bddda8d5963a47d14a40811869131030b" + "reference": "d6807c0b7308e323bd77cced667dee3f2d5e6a82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeinwp/themeisle-sdk/zipball/3c761b0bddda8d5963a47d14a40811869131030b", - "reference": "3c761b0bddda8d5963a47d14a40811869131030b", + "url": "https://api.github.com/repos/Codeinwp/themeisle-sdk/zipball/d6807c0b7308e323bd77cced667dee3f2d5e6a82", + "reference": "d6807c0b7308e323bd77cced667dee3f2d5e6a82", "shasum": "" }, "require-dev": { @@ -43,9 +43,9 @@ ], "support": { "issues": "https://github.com/Codeinwp/themeisle-sdk/issues", - "source": "https://github.com/Codeinwp/themeisle-sdk/tree/v3.3.57" + "source": "https://github.com/Codeinwp/themeisle-sdk/tree/v3.3.58" }, - "time": "2026-07-23T13:31:25+00:00" + "time": "2026-07-29T08:38:52+00:00" }, { "name": "neitanod/forceutf8", From 14c63774a37a6718977d7b5122f6c6d0a6742531 Mon Sep 17 00:00:00 2001 From: Marius Cristea Date: Wed, 2 Sep 2026 13:19:01 +0300 Subject: [PATCH 05/10] ci: skip PR-comment job on Dependabot PRs (no secrets, always fails) Co-Authored-By: Claude Fable 5 --- .github/workflows/build-dev-artifacts.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-dev-artifacts.yml b/.github/workflows/build-dev-artifacts.yml index 8a6a1398a..6aa924027 100755 --- a/.github/workflows/build-dev-artifacts.yml +++ b/.github/workflows/build-dev-artifacts.yml @@ -57,7 +57,7 @@ jobs: comment-on-pr: name: Comment on PR with links to plugin ZIPs - if: ${{ github.head_ref && github.head_ref != null }} + if: ${{ github.head_ref && github.head_ref != null && github.actor != 'dependabot[bot]' }} runs-on: ubuntu-latest needs: dev-zip env: From d7ee33a04ea125b86151b40d8b5835aff109a2c5 Mon Sep 17 00:00:00 2001 From: Marius Cristea Date: Wed, 2 Sep 2026 13:25:06 +0300 Subject: [PATCH 06/10] ci: revert Dependabot gate on PR-comment job (org Dependabot secret covers this repo) [skip ci] Co-Authored-By: Claude Fable 5 --- .github/workflows/build-dev-artifacts.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-dev-artifacts.yml b/.github/workflows/build-dev-artifacts.yml index 6aa924027..8a6a1398a 100755 --- a/.github/workflows/build-dev-artifacts.yml +++ b/.github/workflows/build-dev-artifacts.yml @@ -57,7 +57,7 @@ jobs: comment-on-pr: name: Comment on PR with links to plugin ZIPs - if: ${{ github.head_ref && github.head_ref != null && github.actor != 'dependabot[bot]' }} + if: ${{ github.head_ref && github.head_ref != null }} runs-on: ubuntu-latest needs: dev-zip env: From 150d7300a5f219fa999ab31c0bcd220374a56ad6 Mon Sep 17 00:00:00 2001 From: selul Date: Wed, 2 Sep 2026 13:38:04 +0300 Subject: [PATCH 07/10] fix: keep the unsafe-destination error for non-public IP literals on WP 7.1+ wp_http_validate_url() now rejects link-local/private IP literals itself, which collapsed the visualizer_unsafe_remote_url error into visualizer_invalid_remote_url and broke test_blocks_redirect_to_link_local_destination. Co-Authored-By: Claude Fable 5 --- classes/Visualizer/Remote/Fetch.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/classes/Visualizer/Remote/Fetch.php b/classes/Visualizer/Remote/Fetch.php index 03040c4c9..b1923caec 100644 --- a/classes/Visualizer/Remote/Fetch.php +++ b/classes/Visualizer/Remote/Fetch.php @@ -257,6 +257,15 @@ private static function validate_url( $url, &$ips = array() ) { $ips = array(); $validated_url = wp_http_validate_url( $url ); if ( false === $validated_url ) { + // WordPress 7.1+ rejects non-public IP literals inside wp_http_validate_url() + // itself; older cores let them through to our is_global_ip() check below. Keep + // the distinct "unsafe destination" error on every core version so callers can + // tell a policy block from a malformed URL. + $scheme = strtolower( (string) wp_parse_url( $url, PHP_URL_SCHEME ) ); + $host = (string) wp_parse_url( $url, PHP_URL_HOST ); + if ( in_array( $scheme, array( 'http', 'https' ), true ) && filter_var( $host, FILTER_VALIDATE_IP ) && ! self::is_global_ip( $host ) ) { + return new WP_Error( 'visualizer_unsafe_remote_url', 'The remote URL resolves to a non-public address.' ); + } return new WP_Error( 'visualizer_invalid_remote_url', 'The remote URL is not allowed.' ); } From 016faff2b00dabc948d05d894e804c2cddee75fd Mon Sep 17 00:00:00 2001 From: selul Date: Wed, 2 Sep 2026 13:46:20 +0300 Subject: [PATCH 08/10] fix: load the block editor stylesheet inside the iframed editor canvas WordPress 7.1 always renders the post editor in an iframe, and styles enqueued via enqueue_block_editor_assets only reach the parent document, leaving the visualizer/chart placeholder unstyled and breaking its e2e specs. Registering the stylesheet as the block type's editor_style gets it injected into the canvas on every core version. Co-Authored-By: Claude Fable 5 --- classes/Visualizer/Gutenberg/Block.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/classes/Visualizer/Gutenberg/Block.php b/classes/Visualizer/Gutenberg/Block.php index be55c6236..3f5743551 100644 --- a/classes/Visualizer/Gutenberg/Block.php +++ b/classes/Visualizer/Gutenberg/Block.php @@ -202,8 +202,25 @@ public function enqueue_gutenberg_scripts() { * Hook server side rendering into render callback */ public function register_block_type() { + $asset_path = VISUALIZER_ABSPATH . '/classes/Visualizer/Gutenberg/build/index.asset.php'; + $version = $this->version; + if ( file_exists( $asset_path ) ) { + // @phpstan-ignore-next-line + $asset = require $asset_path; + $version = isset( $asset['version'] ) ? $asset['version'] : $version; + } + if ( ! wp_style_is( 'visualizer-datatables', 'registered' ) ) { + wp_register_style( 'visualizer-datatables', VISUALIZER_ABSURL . 'css/lib/datatables.min.css', array(), Visualizer_Plugin::VERSION ); + } + if ( ! wp_style_is( 'visualizer-gutenberg-block', 'registered' ) ) { + wp_register_style( 'visualizer-gutenberg-block', VISUALIZER_ABSURL . 'classes/Visualizer/Gutenberg/build/style-index.css', array( 'visualizer-datatables' ), $version ); + } register_block_type( 'visualizer/chart', array( + // The editor_style registration is what gets the stylesheet into the + // iframed editor canvas; styles enqueued via enqueue_block_editor_assets + // only reach the parent document. + 'editor_style' => 'visualizer-gutenberg-block', 'render_callback' => array( $this, 'gutenberg_block_callback' ), 'attributes' => array( 'id' => array( From af6c784083578cb9eca699aa9ca066fdf07b8bd3 Mon Sep 17 00:00:00 2001 From: selul Date: Wed, 2 Sep 2026 14:08:41 +0300 Subject: [PATCH 09/10] test(e2e): target the iframed editor canvas in Gutenberg specs WordPress 7.1 always renders the canvas in an iframe, so page.* locators no longer reach block content; editor.canvas works on both old and new cores. The create-chart popup frame is now selected by its admin-ajax src because the canvas iframe would otherwise match first, and Edit Chart is asserted in the parent document where BlockControls render. Co-Authored-By: Claude Fable 5 --- tests/e2e/specs/gutenberg-editor.spec.js | 55 +++++++++++++----------- 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/tests/e2e/specs/gutenberg-editor.spec.js b/tests/e2e/specs/gutenberg-editor.spec.js index c5ad01578..efb916934 100644 --- a/tests/e2e/specs/gutenberg-editor.spec.js +++ b/tests/e2e/specs/gutenberg-editor.spec.js @@ -18,34 +18,37 @@ test.describe( 'Charts with Gutenberg Editor', () => { page.setDefaultTimeout( 5000 ); } ); - test('check available action on block creation', async ( { admin, editor, page } ) => { + test('check available action on block creation', async ( { admin, editor } ) => { await admin.createNewPost(); await editor.insertBlock( { name: 'visualizer/chart'} ); - // Check chart selection options are available. - await expect( page.getByText('Make a new chart or display') ).toBeVisible(); - await expect( page.getByLabel('Editor content').locator('a') ).toBeVisible(); - await expect( page.locator('div').filter({ hasText: /^Display an existing chart$/ }) ).toBeVisible(); + // The block renders inside the editor canvas, which WordPress serves in + // an iframe, so every locator must go through editor.canvas. + await expect( editor.canvas.getByText('Make a new chart or display') ).toBeVisible(); + await expect( editor.canvas.locator('a.visualizer-settings__content-option').filter({ hasText: 'Create a new chart' }) ).toBeVisible(); + await expect( editor.canvas.locator('div').filter({ hasText: /^Display an existing chart$/ }) ).toBeVisible(); } ); test('new chart creation', async ( { admin, editor, page } ) => { await admin.createNewPost(); await editor.insertBlock( { name: 'visualizer/chart'} ); - await expect( page.getByText('Make a new chart or display') ).toBeVisible(); - await expect( page.getByLabel('Editor content').locator('a') ).toBeVisible(); + await expect( editor.canvas.getByText('Make a new chart or display') ).toBeVisible(); + const createOption = editor.canvas.locator('a.visualizer-settings__content-option').filter({ hasText: 'Create a new chart' }); + await expect( createOption ).toBeVisible(); - await page.getByLabel('Editor content').locator('a').click({ force: true}); + await createOption.click({ force: true }); - // Create chart via popup. - await page.frameLocator('iframe').getByRole('button', { name: 'Next' }).click(); - await page.frameLocator('iframe').getByRole('button', { name: 'Create Chart' }).click(); + // Create chart via popup; target the wizard frame, not the canvas iframe. + const wizard = page.frameLocator('iframe[src*="visualizer-create-chart"]'); + await wizard.getByRole('button', { name: 'Next' }).click(); + await wizard.getByRole('button', { name: 'Create Chart' }).click(); - await expect( page.getByRole('button', { name: 'Done' }) ).toBeVisible(); - await page.getByRole('button', { name: 'Done' }).click(); + await expect( editor.canvas.getByRole('button', { name: 'Done' }) ).toBeVisible(); + await editor.canvas.getByRole('button', { name: 'Done' }).click(); - await expect( page.locator('.wp-block-visualizer-chart').count() ).resolves.toBe( 1 ); - await expect( page.getByRole('button', { name: 'Done' }) ).toBeHidden(); + await expect( editor.canvas.locator('.wp-block-visualizer-chart') ).toHaveCount( 1 ); + await expect( editor.canvas.getByRole('button', { name: 'Done' }) ).toBeHidden(); } ); @@ -55,15 +58,15 @@ test.describe( 'Charts with Gutenberg Editor', () => { // Create a new post and insert the first available chart. await admin.createNewPost(); await editor.insertBlock( { name: 'visualizer/chart'} ); - await page.locator('div').filter({ hasText: /^Display an existing chart$/ }).click(); - await page.locator('.visualizer-settings__charts-controls').first().click(); + await editor.canvas.locator('div').filter({ hasText: /^Display an existing chart$/ }).click(); + await editor.canvas.locator('.visualizer-settings__charts-controls').first().click(); // Check if it was inserted correctly then enter view mode for the block. - expect( page.getByLabel('Block: Visualizer Chart').getByText('Visualizer') ).not.toBeNull(); - await page.getByRole('button', { name: 'Done' }).click(); + await expect( editor.canvas.getByLabel('Block: Visualizer Chart') ).toBeVisible(); + await editor.canvas.getByRole('button', { name: 'Done' }).click(); // Check if the Chart did not crash the editor. - expect( page.locator('.wp-block-visualizer-chart').count() ).resolves.toBe( 1 ); + await expect( editor.canvas.locator('.wp-block-visualizer-chart') ).toHaveCount( 1 ); } ); test( 'check block Edit new button', async ( { admin, editor, page } ) => { @@ -73,13 +76,15 @@ test.describe( 'Charts with Gutenberg Editor', () => { await admin.createNewPost(); await editor.insertBlock( { name: 'visualizer/chart'} ); - await page.locator('div').filter({ hasText: /^Display an existing chart$/ }).click(); - await page.locator('.visualizer-settings__charts-controls').first().click(); + await editor.canvas.locator('div').filter({ hasText: /^Display an existing chart$/ }).click(); + await editor.canvas.locator('.visualizer-settings__charts-controls').first().click(); - expect( page.getByLabel('Block: Visualizer Chart').getByText('Visualizer') ).not.toBeNull(); + await expect( editor.canvas.getByLabel('Block: Visualizer Chart') ).toBeVisible(); - await expect(page.getByRole('button', { name: 'Edit Chart' })).toBeVisible(); - await page.getByRole('button', { name: 'Edit Chart' }).click(); + // The Edit Chart button lives in the block toolbar / inspector, which + // render in the parent document, not the canvas iframe. + await expect( page.getByRole('button', { name: 'Edit Chart' }).first() ).toBeVisible(); + await page.getByRole('button', { name: 'Edit Chart' }).first().click(); //await page.goto('http://localhost:8889/wp-admin/post.php?post=29&action=edit'); await expect(page.getByLabel('Visualizer', { exact: true }).locator('h1')).toContainText('Visualizer'); await page.getByRole('button', { name: 'Close dialog' }).click(); From 5d3235ef03d31f54804514150bf6a0bf6a948b45 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Sep 2026 21:01:30 +0300 Subject: [PATCH 10/10] chore(deps): bump codeinwp/themeisle-sdk from 3.3.58 to 3.3.61 (#1371) Bumps [codeinwp/themeisle-sdk](https://github.com/Codeinwp/themeisle-sdk) from 3.3.58 to 3.3.61. - [Release notes](https://github.com/Codeinwp/themeisle-sdk/releases) - [Changelog](https://github.com/Codeinwp/themeisle-sdk/blob/v3.3.61/CHANGELOG.md) - [Commits](https://github.com/Codeinwp/themeisle-sdk/compare/v3.3.58...v3.3.61) --- updated-dependencies: - dependency-name: codeinwp/themeisle-sdk dependency-version: 3.3.61 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- composer.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/composer.lock b/composer.lock index 81e48703f..dd538ab7b 100644 --- a/composer.lock +++ b/composer.lock @@ -8,16 +8,16 @@ "packages": [ { "name": "codeinwp/themeisle-sdk", - "version": "3.3.58", + "version": "3.3.61", "source": { "type": "git", "url": "https://github.com/Codeinwp/themeisle-sdk.git", - "reference": "d6807c0b7308e323bd77cced667dee3f2d5e6a82" + "reference": "9fe698b52dec768a0dd8b500fb51efe40962ee99" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeinwp/themeisle-sdk/zipball/d6807c0b7308e323bd77cced667dee3f2d5e6a82", - "reference": "d6807c0b7308e323bd77cced667dee3f2d5e6a82", + "url": "https://api.github.com/repos/Codeinwp/themeisle-sdk/zipball/9fe698b52dec768a0dd8b500fb51efe40962ee99", + "reference": "9fe698b52dec768a0dd8b500fb51efe40962ee99", "shasum": "" }, "require-dev": { @@ -43,9 +43,9 @@ ], "support": { "issues": "https://github.com/Codeinwp/themeisle-sdk/issues", - "source": "https://github.com/Codeinwp/themeisle-sdk/tree/v3.3.58" + "source": "https://github.com/Codeinwp/themeisle-sdk/tree/v3.3.61" }, - "time": "2026-07-29T08:38:52+00:00" + "time": "2026-08-24T15:59:27+00:00" }, { "name": "neitanod/forceutf8",