diff --git a/MIGRATION.md b/MIGRATION.md new file mode 100644 index 0000000..b94f07e --- /dev/null +++ b/MIGRATION.md @@ -0,0 +1,86 @@ +# Migrating from `beapi/tabs-block` to `blockparty-tabs` + +This document describes how to move from the abandoned [`beapi/tabs-block`](https://github.com/BeAPI/beapi-tabs-block) package to `beapi/blockparty-tabs`. + +## Package swap + +```bash +composer remove beapi/tabs-block +composer require beapi/blockparty-tabs +``` + +Activate `blockparty-tabs` and deactivate `beapi-tabs-block` (or `tabs-block`). + +## Block model changes + +| Old | New | +|---|---| +| `beapi/tabs` | `blockparty/tabs` | +| `beapi/tabs-nav` | `blockparty/tabs-nav` | +| `beapi/tabs-nav-item` | `blockparty/tabs-nav-item` | +| `beapi/tabs-panels` | `blockparty/tabs-panels` | +| `beapi/tabs-panel-item` | `blockparty/tabs-panel-item` | +| Parent `title` (saved as `aria-label` on the root) | `ariaLabel` on `blockparty/tabs-nav` | +| CSS `.wp-block-beapi-tabs` | CSS `.wp-block-blockparty-tabs` | + +Nested structure (nav + panels, paired nav/panel items) is preserved. Icons inside tab labels keep working when `blockparty-icons` or `beapi/icon-block` is active (`blockparty_tabs_allowed_icon_blocks` filter). + +## Content migration (WP-CLI) + +WP-CLI only loads **active** plugins. If you see `'blockparty-tabs' is not a registered wp command`, activate the plugin first: + +```bash +wp plugin activate blockparty-tabs +``` + +After activating `blockparty-tabs` and updating theme allowlists: + +```bash +# Preview (current site) +wp blockparty-tabs migrate-from-tabs-block --dry-run + +# Apply on the current site +wp blockparty-tabs migrate-from-tabs-block + +# Limit post types +wp blockparty-tabs migrate-from-tabs-block --post-type=post,page +``` + +The command always targets **one site only**. On multisite, repeat it yourself with `--url=` for each site: + +```bash +wp blockparty-tabs migrate-from-tabs-block --url=https://example.com/ +``` + +The command: + +1. Scans `post_content` for `beapi/tabs*` block comments and legacy class names +2. Parses blocks, converts markup recursively, serializes back +3. Moves the parent `title` attribute to `ariaLabel` on `blockparty/tabs-nav` +4. Aligns saved HTML with blockparty-tabs `save()` output (ARIA roles, `is-active` / `is-hidden` classes) +5. Updates posts with `wp_slash()` on content +6. Logs migrated / skipped counts + +### Revisions + +Only the scanned post types are migrated (public REST post types + `wp_block` by default). **Revisions are not included** unless you pass them explicitly: + +```bash +wp blockparty-tabs migrate-from-tabs-block --post-type=revision +``` + +### Duplicate Post (Yoast) + +If [Yoast Duplicate Post](https://wordpress.org/plugins/duplicate-post/) is active, updating a Rewrite & Republish **copy** via `wp_update_post()` can abort the WP-CLI run. + +Preferred workaround: skip that plugin for the migration only: + +```bash +wp --skip-plugins=duplicate-post blockparty-tabs migrate-from-tabs-block --url=https://example.com/ +``` + +## Front-end checklist + +- Update theme allowlists from `beapi/tabs*` to `blockparty/tabs*` +- Update theme SCSS selectors from `.wp-block-beapi-tabs` to `.wp-block-blockparty-tabs` +- Smoke-test top alignment and sidebar layouts, keyboard navigation, and nested tabs diff --git a/blockparty-tabs.php b/blockparty-tabs.php index 02e0a1d..1cd8c03 100644 --- a/blockparty-tabs.php +++ b/blockparty-tabs.php @@ -14,6 +14,12 @@ namespace Blockparty\Tabs; +use Blockparty\Tabs\Cli\MigrateFromTabsBlockCommand; + +if ( is_readable( __DIR__ . '/vendor/autoload.php' ) ) { + include_once __DIR__ . '/vendor/autoload.php'; +} + define( 'BLOCKPARTY_TABS_VERSION', '2.0.1' ); define( 'BLOCKPARTY_TABS_URL', plugin_dir_url( __FILE__ ) ); define( 'BLOCKPARTY_TABS_DIR', plugin_dir_path( __FILE__ ) ); @@ -174,3 +180,7 @@ function register_react_jsx_runtime( $scripts ): void { } add_action( 'wp_default_scripts', __NAMESPACE__ . '\\register_react_jsx_runtime' ); + +if ( defined( 'WP_CLI' ) && WP_CLI && class_exists( MigrateFromTabsBlockCommand::class ) ) { + \WP_CLI::add_command( 'blockparty-tabs migrate-from-tabs-block', MigrateFromTabsBlockCommand::class ); +} diff --git a/composer.json b/composer.json index da67fe1..db4d0eb 100644 --- a/composer.json +++ b/composer.json @@ -1,50 +1,60 @@ { - "name": "beapi/blockparty-tabs", - "description": "Accessible Tabs block for WordPress gutenberg.", - "type": "wordpress-plugin", - "keywords": ["wordpress", "plugin", "tabs", "gutenberg"], - "license": "GPL-2.0-or-later", - "authors": [ - { - "name": "BeAPI", - "email": "technique@beapi.fr" - } - ], - "config": { - "optimize-autoloader": true, - "preferred-install": "dist", - "sort-packages": true, - "allow-plugins": { - "dealerdirect/phpcodesniffer-composer-installer": true, - "composer/installers": true, - "phpro/grumphp-shim": true - }, - "platform": { - "php": "8.1.99" - } - }, - "require": { - "php": "^8.1", - "composer/installers": "^1.0 || ^2.0" - }, - "require-dev": { - "dealerdirect/phpcodesniffer-composer-installer": "^1.0", - "overtrue/phplint": "^9.1", - "php-parallel-lint/php-parallel-lint": "^1.3", - "phpcompatibility/phpcompatibility-wp": "^2.1", - "phpro/grumphp-shim": "^1.5", - "phpunit/phpunit": "9.6.37", - "roave/security-advisories": "dev-latest", - "wp-coding-standards/wpcs": "^3.0", - "wp-phpunit/wp-phpunit": "7.1.1", - "yoast/phpunit-polyfills": "4.0.0" - }, - "scripts": { - "cs": "./vendor/bin/phpcs", - "cb": "./vendor/bin/phpcbf" - }, - "scripts-descriptions": { - "cs": "Run PHP CodeSniffer on codebase using custom ruleset.", - "cb": "Run PHP Code Beautifier and Fixer on codebase using custom ruleset." - } -} + "name": "beapi/blockparty-tabs", + "description": "Accessible Tabs block for WordPress gutenberg.", + "type": "wordpress-plugin", + "keywords": [ + "wordpress", + "plugin", + "tabs", + "gutenberg" + ], + "license": "GPL-2.0-or-later", + "authors": [ + { + "name": "BeAPI", + "email": "technique@beapi.fr" + } + ], + "config": { + "optimize-autoloader": true, + "preferred-install": "dist", + "sort-packages": true, + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true, + "composer/installers": true, + "phpro/grumphp-shim": true + }, + "platform": { + "php": "8.1.99" + } + }, + "autoload": { + "psr-4": { + "Blockparty\\Tabs\\": "includes/" + } + }, + "require": { + "php": "^8.1", + "composer/installers": "^1.0 || ^2.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "overtrue/phplint": "^9.1", + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpcompatibility/phpcompatibility-wp": "^2.1", + "phpro/grumphp-shim": "^1.5", + "phpunit/phpunit": "9.6.37", + "roave/security-advisories": "dev-latest", + "wp-coding-standards/wpcs": "^3.0", + "wp-phpunit/wp-phpunit": "7.1.1", + "yoast/phpunit-polyfills": "4.0.0" + }, + "scripts": { + "cs": "./vendor/bin/phpcs", + "cb": "./vendor/bin/phpcbf" + }, + "scripts-descriptions": { + "cs": "Run PHP CodeSniffer on codebase using custom ruleset.", + "cb": "Run PHP Code Beautifier and Fixer on codebase using custom ruleset." + } +} \ No newline at end of file diff --git a/includes/Cli/MigrateFromTabsBlockCommand.php b/includes/Cli/MigrateFromTabsBlockCommand.php new file mode 100644 index 0000000..9b4d3ae --- /dev/null +++ b/includes/Cli/MigrateFromTabsBlockCommand.php @@ -0,0 +1,259 @@ +] + * : Comma-separated list of post types. Default: any public post type that shows in the REST API, plus wp_block. + * + * [--posts-per-page=] + * : Batch size. Default: 100. + * + * ## EXAMPLES + * + * wp blockparty-tabs migrate-from-tabs-block --dry-run + * wp blockparty-tabs migrate-from-tabs-block --post-type=post,page + * wp blockparty-tabs migrate-from-tabs-block --url=https://example.com/ + * + * @subcommand migrate-from-tabs-block + * + * @param array $args Positional arguments. + * @param array $assoc_args Associative arguments. + * @return void + */ + public function __invoke( $args, $assoc_args ): void { + $dry_run = (bool) WP_CLI\Utils\get_flag_value( $assoc_args, 'dry-run', false ); + $posts_per_page = (int) ( $assoc_args['posts-per-page'] ?? 100 ); + $post_types = $this->resolve_post_types( $assoc_args['post-type'] ?? '' ); + + if ( empty( $post_types ) ) { + WP_CLI::error( 'No post types to migrate.' ); + } + + WP_CLI::log( + sprintf( + 'Migrating site %1$d (%2$s)%3$s — post types: %4$s', + get_current_blog_id(), + home_url( '/' ), + $dry_run ? ' [dry-run]' : '', + implode( ', ', $post_types ) + ) + ); + + $migrator = new TabsBlockMigrator(); + $posts_updated = 0; + $posts_scanned = 0; + $page = 1; + $max_pages = 1; + $total_migrated = 0; + $total_skipped = 0; + + while ( $page <= $max_pages ) { + $query = new WP_Query( + [ + 'post_type' => $post_types, + 'post_status' => 'any', + 'posts_per_page' => $posts_per_page, + 'paged' => $page, + 'orderby' => 'ID', + 'order' => 'ASC', + 'ignore_sticky_posts' => true, + 'no_found_rows' => false, + 'update_post_meta_cache' => false, + 'update_post_term_cache' => false, + ] + ); + + $max_pages = (int) $query->max_num_pages; + + if ( ! $query->have_posts() ) { + break; + } + + foreach ( $query->posts as $post ) { + ++$posts_scanned; + + if ( ! is_string( $post->post_content ) || '' === $post->post_content ) { + continue; + } + + if ( ! $this->post_has_legacy_tabs( $post->post_content ) ) { + continue; + } + + $before_migrated = $migrator->migrated; + $before_skipped = $migrator->skipped; + $new_content = $migrator->migrate_content( $post->post_content ); + + $delta_migrated = $migrator->migrated - $before_migrated; + $delta_skipped = $migrator->skipped - $before_skipped; + + if ( null === $new_content ) { + continue; + } + + $label = '[dry-run]'; + if ( ! $dry_run ) { + $status = $this->persist_post_content( (int) $post->ID, $new_content ); + if ( '' === $status ) { + continue; + } + $label = '[' . $status . ']'; + } + + $total_migrated += $delta_migrated; + $total_skipped += $delta_skipped; + ++$posts_updated; + + WP_CLI::log( + sprintf( + '%1$s post %2$d (%3$s) — blocks migrated: %4$d, skipped: %5$d', + $label, + (int) $post->ID, + $post->post_type, + $delta_migrated, + $delta_skipped + ) + ); + } + + ++$page; + wp_reset_postdata(); + } + + WP_CLI::success( + sprintf( + 'Done. Posts scanned: %1$d, posts %2$s: %3$d, tab blocks migrated: %4$d, skipped: %5$d.', + $posts_scanned, + $dry_run ? 'that would update' : 'updated', + $posts_updated, + $total_migrated, + $total_skipped + ) + ); + } + + /** + * @param string $content Post content. + * @return bool + */ + private function post_has_legacy_tabs( string $content ): bool { + return false !== strpos( $content, 'beapi/tabs' ); + } + + /** + * Save migrated content, with a content-only fallback for orphaned page templates. + * + * @param int $post_id Post ID. + * @param string $new_content Unslashed post content. + * @return string Empty on failure, `update` or `update-fallback` on success. + */ + private function persist_post_content( int $post_id, string $new_content ): string { + $updated = wp_update_post( + [ + 'ID' => $post_id, + 'post_content' => wp_slash( $new_content ), + ], + true + ); + + if ( ! is_wp_error( $updated ) ) { + return 'update'; + } + + if ( 'invalid_page_template' === $updated->get_error_code() && $this->update_post_content_fallback( $post_id, $new_content ) ) { + return 'update-fallback'; + } + + WP_CLI::warning( + sprintf( + 'Failed to update post %1$d: %2$s', + $post_id, + $updated->get_error_message() + ) + ); + + return ''; + } + + /** + * Update post_content only, bypassing page template validation. + * + * @param int $post_id Post ID. + * @param string $new_content Unslashed post content. + * @return bool + */ + private function update_post_content_fallback( int $post_id, string $new_content ): bool { + global $wpdb; + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Content-only fallback when wp_update_post() rejects an orphaned _wp_page_template. + $result = $wpdb->update( + $wpdb->posts, + [ + 'post_content' => $new_content, + ], + [ + 'ID' => $post_id, + ], + [ '%s' ], + [ '%d' ] + ); + + if ( false === $result ) { + return false; + } + + clean_post_cache( $post_id ); + + return true; + } + + /** + * Resolve target post types. + * + * @param string $raw Comma-separated post types from CLI. + * @return string[] + */ + private function resolve_post_types( string $raw ): array { + if ( '' !== trim( $raw ) ) { + $types = array_filter( array_map( 'trim', explode( ',', $raw ) ) ); + + return array_values( $types ); + } + + $types = get_post_types( + [ + 'public' => true, + 'show_in_rest' => true, + ], + 'names' + ); + + $types[] = 'wp_block'; + $types = array_unique( array_values( $types ) ); + + return $types; + } +} diff --git a/includes/Migration/TabsBlockMigrator.php b/includes/Migration/TabsBlockMigrator.php new file mode 100644 index 0000000..f209ace --- /dev/null +++ b/includes/Migration/TabsBlockMigrator.php @@ -0,0 +1,342 @@ + + */ + private const BLOCK_MAP = [ + 'beapi/tabs' => 'blockparty/tabs', + 'beapi/tabs-nav' => 'blockparty/tabs-nav', + 'beapi/tabs-nav-item' => 'blockparty/tabs-nav-item', + 'beapi/tabs-panels' => 'blockparty/tabs-panels', + 'beapi/tabs-panel-item' => 'blockparty/tabs-panel-item', + ]; + + /** + * Class replacements (longest keys first). + * + * @var array + */ + private const CLASS_MAP = [ + 'wp-block-beapi-tabs-panel-item__inner' => 'wp-block-blockparty-tabs-panel-item__inner', + 'wp-block-beapi-tabs-nav-link' => 'wp-block-blockparty-tabs-nav-link', + 'wp-block-beapi-tabs-nav-item' => 'wp-block-blockparty-tabs-nav-item', + 'wp-block-beapi-tabs-panel-item' => 'wp-block-blockparty-tabs-panel-item', + 'wp-block-beapi-tabs-panels' => 'wp-block-blockparty-tabs-panels', + 'wp-block-beapi-tabs-nav' => 'wp-block-blockparty-tabs-nav', + 'wp-block-beapi-tabs' => 'wp-block-blockparty-tabs', + ]; + + /** + * Entry point: rewrite post content, or return null if nothing to do. + * + * @param string $content Post content. + * @return string|null + */ + public function migrate_content( string $content ): ?string { + if ( ! $this->content_has_legacy_tabs( $content ) ) { + return null; + } + + $new = serialize_blocks( $this->migrate_blocks( parse_blocks( $content ) ) ); + + return $new === $content ? null : $new; + } + + /** + * @param string $content Post content. + * @return bool + */ + private function content_has_legacy_tabs( string $content ): bool { + foreach ( array_keys( self::BLOCK_MAP ) as $legacy_name ) { + if ( false !== strpos( $content, $legacy_name ) ) { + return true; + } + } + + return false; + } + + /** + * Walk every block. Rename tabs blocks and fix saved markup. + * + * @param array> $blocks Parsed blocks. + * @return array> + */ + private function migrate_blocks( array $blocks ): array { + $out = []; + + foreach ( $blocks as $block ) { + $name = $block['blockName'] ?? ''; + + if ( ! empty( $block['innerBlocks'] ) ) { + $block['innerBlocks'] = $this->migrate_blocks( $block['innerBlocks'] ); + } + + if ( isset( self::BLOCK_MAP[ $name ] ) ) { + $block = $this->convert_block( $block, $name ); + ++$this->migrated; + } + + $out[] = $block; + } + + return $out; + } + + /** + * @param array $block Parsed block. + * @param string $legacy_name Legacy block name. + * @return array + */ + private function convert_block( array $block, string $legacy_name ): array { + $attrs = is_array( $block['attrs'] ?? null ) ? $block['attrs'] : []; + $title = ''; + + if ( 'beapi/tabs' === $legacy_name ) { + $title = (string) ( $attrs['title'] ?? '' ); + unset( $attrs['title'] ); + $block['innerBlocks'] = $this->apply_title_to_nav( $block['innerBlocks'] ?? [], $title ); + } + + $block['blockName'] = self::BLOCK_MAP[ $legacy_name ]; + $block['attrs'] = $attrs; + + $this->rewrite_block_markup( $block, $block['blockName'], $attrs ); + + return $block; + } + + /** + * Move legacy parent title onto the tabs-nav block as ariaLabel. + * + * @param array> $inner_blocks Nav / panels children. + * @param string $title Parent title attribute. + * @return array> + */ + private function apply_title_to_nav( array $inner_blocks, string $title ): array { + if ( '' === $title ) { + return $inner_blocks; + } + + foreach ( $inner_blocks as $index => $inner ) { + if ( 'blockparty/tabs-nav' !== ( $inner['blockName'] ?? '' ) ) { + continue; + } + + $nav_attrs = is_array( $inner['attrs'] ?? null ) ? $inner['attrs'] : []; + if ( empty( $nav_attrs['ariaLabel'] ) ) { + $nav_attrs['ariaLabel'] = $title; + $inner['attrs'] = $nav_attrs; + $this->rewrite_block_markup( $inner, 'blockparty/tabs-nav', $nav_attrs ); + } + + $inner_blocks[ $index ] = $inner; + break; + } + + return $inner_blocks; + } + + /** + * Replace legacy classes and adjust ARIA in innerHTML / innerContent. + * + * @param array $block Parsed block (by reference). + * @param string $block_name New block name. + * @param array $attrs Block attributes. + * @return void + */ + private function rewrite_block_markup( array &$block, string $block_name, array $attrs ): void { + if ( isset( $block['innerHTML'] ) && is_string( $block['innerHTML'] ) ) { + $block['innerHTML'] = $this->transform_markup( $block['innerHTML'], $block_name, $attrs ); + } + + if ( empty( $block['innerContent'] ) || ! is_array( $block['innerContent'] ) ) { + return; + } + + foreach ( $block['innerContent'] as $index => $chunk ) { + if ( ! is_string( $chunk ) || '' === $chunk ) { + continue; + } + + $block['innerContent'][ $index ] = $this->transform_markup( $chunk, $block_name, $attrs ); + } + } + + /** + * @param string $html Markup fragment. + * @param string $block_name Block name after migration. + * @param array $attrs Block attributes. + * @return string + */ + private function transform_markup( string $html, string $block_name, array $attrs ): string { + $html = $this->replace_classes( $html ); + + switch ( $block_name ) { + case 'blockparty/tabs': + return $this->transform_tabs_root_markup( $html ); + case 'blockparty/tabs-nav': + return $this->transform_tabs_nav_markup( $html, $attrs ); + case 'blockparty/tabs-nav-item': + return $this->transform_tabs_nav_item_markup( $html, $attrs ); + case 'blockparty/tabs-panel-item': + return $this->transform_tabs_panel_item_markup( $html, $attrs ); + default: + return $html; + } + } + + /** + * @param string $html Markup. + * @return string + */ + private function replace_classes( string $html ): string { + return str_replace( array_keys( self::CLASS_MAP ), array_values( self::CLASS_MAP ), $html ); + } + + /** + * @param string $html Markup. + * @return string + */ + private function transform_tabs_root_markup( string $html ): string { + if ( ! class_exists( 'WP_HTML_Tag_Processor' ) ) { + return preg_replace( '/\srole=(["\'])tablist\1/i', '', $html ) ?? $html; + } + + $processor = new \WP_HTML_Tag_Processor( $html ); + if ( $processor->next_tag() ) { + $processor->remove_attribute( 'role' ); + $processor->remove_attribute( 'aria-label' ); + } + + return $processor->get_updated_html(); + } + + /** + * @param string $html Markup. + * @param array $attrs Block attributes. + * @return string + */ + private function transform_tabs_nav_markup( string $html, array $attrs ): string { + $label = (string) ( $attrs['ariaLabel'] ?? '' ); + + if ( ! class_exists( 'WP_HTML_Tag_Processor' ) ) { + return $html; + } + + $processor = new \WP_HTML_Tag_Processor( $html ); + if ( $processor->next_tag( [ 'tag_name' => 'UL' ] ) ) { + $processor->set_attribute( 'role', 'tablist' ); + if ( '' !== $label ) { + $processor->set_attribute( 'aria-label', $label ); + } + } + + return $processor->get_updated_html(); + } + + /** + * @param string $html Markup. + * @param array $attrs Block attributes. + * @return string + */ + private function transform_tabs_nav_item_markup( string $html, array $attrs ): string { + $index = (int) ( $attrs['index'] ?? 0 ); + $is_active = 0 === $index; + + if ( ! class_exists( 'WP_HTML_Tag_Processor' ) ) { + return $html; + } + + $processor = new \WP_HTML_Tag_Processor( $html ); + while ( $processor->next_tag() ) { + if ( 'LI' === $processor->get_tag() ) { + $processor->set_attribute( 'class', $this->toggle_state_class( (string) $processor->get_attribute( 'class' ), 'is-active', $is_active ) ); + } + + if ( 'A' === $processor->get_tag() && 'tab' === $processor->get_attribute( 'role' ) ) { + $processor->set_attribute( 'aria-selected', $is_active ? 'true' : 'false' ); + if ( $is_active ) { + $processor->remove_attribute( 'tabindex' ); + } else { + $processor->set_attribute( 'tabindex', '-1' ); + } + } + } + + return $processor->get_updated_html(); + } + + /** + * @param string $html Markup. + * @param array $attrs Block attributes. + * @return string + */ + private function transform_tabs_panel_item_markup( string $html, array $attrs ): string { + $index = (int) ( $attrs['index'] ?? 0 ); + $is_active = 0 === $index; + + if ( ! class_exists( 'WP_HTML_Tag_Processor' ) ) { + return $html; + } + + $processor = new \WP_HTML_Tag_Processor( $html ); + while ( $processor->next_tag() ) { + if ( 'DIV' !== $processor->get_tag() || 'tabpanel' !== $processor->get_attribute( 'role' ) ) { + continue; + } + + $class = (string) $processor->get_attribute( 'class' ); + $class = $this->toggle_state_class( $class, 'is-active', $is_active ); + $class = $this->toggle_state_class( $class, 'is-hidden', ! $is_active ); + $processor->set_attribute( 'class', $class ); + } + + return $processor->get_updated_html(); + } + + /** + * @param string $class Existing class string. + * @param string $state_class State class to add or remove. + * @param bool $enable Whether the state class should be present. + * @return string + */ + private function toggle_state_class( string $class, string $state_class, bool $enable ): string { + $parts = array_filter( preg_split( '/\s+/', trim( $class ) ) ?: [] ); + $parts = array_values( array_diff( $parts, [ $state_class ] ) ); + + if ( $enable ) { + $parts[] = $state_class; + } + + return implode( ' ', $parts ); + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 6c00134..d9ec446 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -18,6 +18,7 @@ blockparty-tabs.php + includes diff --git a/tests/php/TabsBlockMigratorTest.php b/tests/php/TabsBlockMigratorTest.php new file mode 100644 index 0000000..e37f68e --- /dev/null +++ b/tests/php/TabsBlockMigratorTest.php @@ -0,0 +1,123 @@ + +
+ + + + +
+
+

Panel one

+
+ + + +
+

Panel two

+
+
+
+ +CONTENT; + + $migrator = new TabsBlockMigrator(); + $result = $migrator->migrate_content( $content ); + + $this->assertNotNull( $result ); + $this->assertStringContainsString( 'blockparty/tabs', $result ); + $this->assertStringNotContainsString( 'beapi/tabs', $result ); + $this->assertStringContainsString( 'wp-block-blockparty-tabs', $result ); + $this->assertStringNotContainsString( 'wp-block-beapi-tabs', $result ); + $this->assertStringNotContainsString( '"title":"Our sections"', $result ); + $this->assertStringContainsString( '"ariaLabel":"Our sections"', $result ); + $this->assertStringContainsString( 'role="tablist"', $result ); + $this->assertStringNotContainsString( 'wp-block-blockparty-tabs has-align-top-left" aria-label', $result ); + $this->assertStringContainsString( 'aria-selected="true"', $result ); + $this->assertStringContainsString( 'aria-selected="false"', $result ); + $this->assertStringContainsString( 'tabindex="-1"', $result ); + $this->assertStringContainsString( 'wp-block-blockparty-tabs-panel-item is-active', $result ); + $this->assertStringContainsString( 'wp-block-blockparty-tabs-panel-item is-hidden', $result ); + $this->assertStringContainsString( 'wp-block-blockparty-tabs-nav-item is-active', $result ); + $this->assertGreaterThan( 0, $migrator->migrated ); + } + + /** + * @return void + */ + public function test_migrates_nested_tabs_inside_panel(): void { + $content = <<<'CONTENT' + +
+ + + + +
+
+
+ + + + +
+
+

Nested

+
+
+
+
+
+
+ +CONTENT; + + $migrator = new TabsBlockMigrator(); + $result = $migrator->migrate_content( $content ); + + $this->assertNotNull( $result ); + $this->assertSame( 10, $migrator->migrated ); + $this->assertStringNotContainsString( 'beapi/tabs', $result ); + $this->assertStringContainsString( 'blockparty/tabs', $result ); + } + + /** + * @return void + */ + public function test_returns_null_when_no_legacy_tabs(): void { + $migrator = new TabsBlockMigrator(); + $content = '

Hello

'; + + $this->assertNull( $migrator->migrate_content( $content ) ); + } +}