Commit 4043acea3f for woocommerce

commit 4043acea3f1d8de6d66ef4b3a9a622dc4d19b51f
Author: Allison Levine <1689238+allilevine@users.noreply.github.com>
Date:   Thu Sep 18 11:07:20 2025 -0400

    Email Editor: Add video block and video embed email rendering (#60957)

    * Email Editor: Add video block email rendering with YouTube embed support

    - Add Video block renderer that extends Cover block for email-friendly video thumbnails
    - Add YouTube embed support to Embed block renderer with thumbnail extraction
    - Add play button icon and proper email-compatible styling
    - Add comprehensive test coverage for YouTube embeds and video blocks
    - Update Embed block to detect and render video providers as video blocks
    - Add changelog entry for video email rendering feature

    This enables video blocks and YouTube embeds to render properly in emails
    with thumbnail images and play button overlays using email-compatible HTML.

    * Move play icon to the correct directory.

    * Refactor embed class to reduce repitition.

    * Link to video and fall back to post link.

    * Add clarifying comment and noreferrer.

diff --git a/packages/php/email-editor/changelog/add-video-email-rendering b/packages/php/email-editor/changelog/add-video-email-rendering
new file mode 100644
index 0000000000..24777d2861
--- /dev/null
+++ b/packages/php/email-editor/changelog/add-video-email-rendering
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add email rendering instructions for the core/video block and YouTube embeds.
diff --git a/packages/php/email-editor/src/Integrations/Core/Renderer/Blocks/class-embed.php b/packages/php/email-editor/src/Integrations/Core/Renderer/Blocks/class-embed.php
index 893b294b50..f28e8b4309 100644
--- a/packages/php/email-editor/src/Integrations/Core/Renderer/Blocks/class-embed.php
+++ b/packages/php/email-editor/src/Integrations/Core/Renderer/Blocks/class-embed.php
@@ -10,13 +10,120 @@ namespace Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks;

 use Automattic\WooCommerce\EmailEditor\Engine\Renderer\ContentRenderer\Rendering_Context;
 use Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks\Audio;
+use Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks\Video;
+use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Dom_Document_Helper;
 use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Html_Processing_Helper;

 /**
  * Embed block renderer.
- * This renderer handles core/embed blocks, specifically detecting audio provider embeds (Spotify, SoundCloud, Pocket Casts, Mixcloud, ReverbNation) and rendering them as audio players.
+ * This renderer handles core/embed blocks, detecting audio and video provider embeds and rendering them appropriately.
+ *
+ * Audio providers: Spotify, SoundCloud, Pocket Casts, Mixcloud, ReverbNation - rendered as audio players.
+ * Video providers: YouTube - rendered as video thumbnails with play buttons.
  */
 class Embed extends Abstract_Block_Renderer {
+	/**
+	 * Supported audio providers with their configuration.
+	 *
+	 * @var array
+	 */
+	private const AUDIO_PROVIDERS = array(
+		'pocket-casts' => array(
+			'domains'  => array( 'pca.st' ),
+			'base_url' => 'https://pca.st/',
+		),
+		'spotify'      => array(
+			'domains'  => array( 'open.spotify.com' ),
+			'base_url' => 'https://open.spotify.com/',
+		),
+		'soundcloud'   => array(
+			'domains'  => array( 'soundcloud.com' ),
+			'base_url' => 'https://soundcloud.com/',
+		),
+		'mixcloud'     => array(
+			'domains'  => array( 'mixcloud.com' ),
+			'base_url' => 'https://www.mixcloud.com/',
+		),
+		'reverbnation' => array(
+			'domains'  => array( 'reverbnation.com' ),
+			'base_url' => 'https://www.reverbnation.com/',
+		),
+	);
+
+	/**
+	 * Supported video providers with their configuration.
+	 *
+	 * @var array
+	 */
+	private const VIDEO_PROVIDERS = array(
+		'youtube' => array(
+			'domains'  => array( 'youtube.com', 'youtu.be' ),
+			'base_url' => 'https://www.youtube.com/',
+		),
+	);
+
+	/**
+	 * Get all supported providers (audio and video).
+	 *
+	 * @return array All supported providers.
+	 */
+	private function get_all_supported_providers(): array {
+		return array_merge( array_keys( self::AUDIO_PROVIDERS ), array_keys( self::VIDEO_PROVIDERS ) );
+	}
+
+	/**
+	 * Get all provider configurations (audio and video).
+	 *
+	 * @return array All provider configurations.
+	 */
+	private function get_all_provider_configs(): array {
+		return array_merge( self::AUDIO_PROVIDERS, self::VIDEO_PROVIDERS );
+	}
+
+	/**
+	 * Detect provider from content by checking against known domains.
+	 *
+	 * @param string $content Content to check for provider domains.
+	 * @return string Provider name or empty string if not found.
+	 */
+	private function detect_provider_from_domains( string $content ): string {
+		$all_providers = $this->get_all_provider_configs();
+
+		foreach ( $all_providers as $provider => $config ) {
+			foreach ( $config['domains'] as $domain ) {
+				if ( strpos( $content, $domain ) !== false ) {
+					return $provider;
+				}
+			}
+		}
+
+		return '';
+	}
+
+	/**
+	 * Validate URL using both filter_var and wp_http_validate_url.
+	 *
+	 * @param string $url URL to validate.
+	 * @return bool True if URL is valid.
+	 */
+	private function is_valid_url( string $url ): bool {
+		return ! empty( $url ) && filter_var( $url, FILTER_VALIDATE_URL ) && wp_http_validate_url( $url );
+	}
+
+	/**
+	 * Create fallback attributes for link rendering.
+	 *
+	 * @param string $url URL for the fallback.
+	 * @param string $label Label for the fallback.
+	 * @return array Fallback attributes.
+	 */
+	private function create_fallback_attributes( string $url, string $label ): array {
+		return array(
+			'url'   => $url,
+			'label' => $label,
+		);
+	}
+
 	/**
 	 * Renders the embed block.
 	 *
@@ -34,21 +141,20 @@ class Embed extends Abstract_Block_Renderer {

 		$attr = $parsed_block['attrs'];

-		// Check if this is a supported audio provider embed and has a valid URL.
+		// Check if this is a supported audio or video provider embed and has a valid URL.
 		$provider = $this->get_supported_provider( $attr, $block_content );
 		if ( empty( $provider ) ) {
-			// For non-audio embeds, try to render as a simple link fallback.
+			// For non-supported embeds, try to render as a simple link fallback.
 			return $this->render_link_fallback( $attr, $block_content, $parsed_block, $rendering_context );
 		}

-		$url = $this->extract_provider_url( $attr, $block_content, $provider );
+		$url = $this->extract_provider_url( $attr, $block_content );
 		if ( empty( $url ) ) {
 			// Provider was detected but URL extraction failed - provide graceful fallback.
 			return $this->render_link_fallback( $attr, $block_content, $parsed_block, $rendering_context );
 		}

-		// If we have a valid audio provider embed, proceed with normal rendering.
-		// Note: Audio::render already wraps its output with add_spacer, so we return directly.
+		// If we have a valid audio or video provider embed, proceed with normal rendering.
 		return $this->render_content( $block_content, $parsed_block, $rendering_context );
 	}

@@ -65,9 +171,14 @@ class Embed extends Abstract_Block_Renderer {

 		// Get provider and URL (validation already done in render method).
 		$provider = $this->get_supported_provider( $attr, $block_content );
-		$url      = $this->extract_provider_url( $attr, $block_content, $provider );
+		$url      = $this->extract_provider_url( $attr, $block_content );
+
+		// Check if this is a video provider - render as video block.
+		if ( $this->is_video_provider( $provider ) ) {
+			return $this->render_video_embed( $url, $provider, $parsed_block, $rendering_context, $block_content );
+		}

-		// Get appropriate label for the provider.
+		// For audio providers, use the original audio rendering logic.
 		$label = $this->get_provider_label( $provider, $attr );

 		// Create a mock audio block structure to reuse the Audio renderer.
@@ -91,12 +202,7 @@ class Embed extends Abstract_Block_Renderer {

 		// If audio rendering fails, fall back to a simple link.
 		if ( empty( $audio_result ) ) {
-			// Use the existing render_link_fallback method for consistent spacing and formatting.
-			// Create a mock attr array with the URL and label for the fallback method.
-			$fallback_attr = array(
-				'url'   => $url,
-				'label' => $label,
-			);
+			$fallback_attr = $this->create_fallback_attributes( $url, $label );
 			return $this->render_link_fallback( $fallback_attr, $block_content, $parsed_block, $rendering_context );
 		}

@@ -104,17 +210,17 @@ class Embed extends Abstract_Block_Renderer {
 	}

 	/**
-	 * Get supported audio provider from block attributes or content.
+	 * Get supported audio or video provider from block attributes or content.
 	 *
 	 * @param array  $attr Block attributes.
 	 * @param string $block_content Block content.
 	 * @return string Provider name or empty string if not supported.
 	 */
 	private function get_supported_provider( array $attr, string $block_content ): string {
-		$supported_providers = array( 'pocket-casts', 'spotify', 'soundcloud', 'mixcloud', 'reverbnation' );
+		$all_supported_providers = $this->get_all_supported_providers();

 		// Check provider name slug.
-		if ( isset( $attr['providerNameSlug'] ) && in_array( $attr['providerNameSlug'], $supported_providers, true ) ) {
+		if ( isset( $attr['providerNameSlug'] ) && in_array( $attr['providerNameSlug'], $all_supported_providers, true ) ) {
 			return $attr['providerNameSlug'];
 		}

@@ -122,20 +228,36 @@ class Embed extends Abstract_Block_Renderer {
 		$url              = $attr['url'] ?? '';
 		$content_to_check = ! empty( $url ) ? $url : $block_content;

-		if ( strpos( $content_to_check, 'open.spotify.com' ) !== false ) {
-			return 'spotify';
-		}
-		if ( strpos( $content_to_check, 'soundcloud.com' ) !== false ) {
-			return 'soundcloud';
-		}
-		if ( strpos( $content_to_check, 'pca.st' ) !== false ) {
-			return 'pocket-casts';
-		}
-		if ( strpos( $content_to_check, 'mixcloud.com' ) !== false ) {
-			return 'mixcloud';
-		}
-		if ( strpos( $content_to_check, 'reverbnation.com' ) !== false ) {
-			return 'reverbnation';
+		// Use sophisticated domain detection logic.
+		return $this->detect_provider_from_domains( $content_to_check );
+	}
+
+	/**
+	 * Extract URL from block content using DOM parsing.
+	 *
+	 * @param string $block_content Block content HTML.
+	 * @return string Extracted URL or empty string.
+	 */
+	private function extract_url_from_content( string $block_content ): string {
+		$dom_helper = new Dom_Document_Helper( $block_content );
+
+		// Find the wp-block-embed__wrapper div.
+		$wrapper_element = $dom_helper->find_element( 'div' );
+		if ( $wrapper_element ) {
+			// Check if this div has the correct class.
+			$class_attr = $dom_helper->get_attribute_value( $wrapper_element, 'class' );
+			if ( strpos( $class_attr, 'wp-block-embed__wrapper' ) !== false ) {
+				// Get the text content (URL) from the div.
+				$url = trim( $wrapper_element->textContent ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
+
+				// Decode HTML entities and validate URL.
+				$url = html_entity_decode( $url, ENT_QUOTES | ENT_HTML5, 'UTF-8' );
+
+				// Validate the extracted URL.
+				if ( $this->is_valid_url( $url ) ) {
+					return $url;
+				}
+			}
 		}

 		return '';
@@ -146,31 +268,21 @@ class Embed extends Abstract_Block_Renderer {
 	 *
 	 * @param array  $attr Block attributes.
 	 * @param string $block_content Block content.
-	 * @param string $provider Provider name.
 	 * @return string Provider URL or empty string.
 	 */
-	private function extract_provider_url( array $attr, string $block_content, string $provider ): string {
+	private function extract_provider_url( array $attr, string $block_content ): string {
 		// First, try to get URL from attributes.
 		if ( ! empty( $attr['url'] ) ) {
 			$url = $attr['url'];
 			// Validate the URL from attributes.
-			if ( filter_var( $url, FILTER_VALIDATE_URL ) && wp_http_validate_url( $url ) ) {
+			if ( $this->is_valid_url( $url ) ) {
 				return $url;
 			}
 			return '';
 		}

-		// If not in attributes, extract from block content using simple pattern.
-		// The innerHTML always contains the URL in a predictable structure.
-		if ( preg_match( '/<div class="wp-block-embed__wrapper">([^<]+)<\/div>/', $block_content, $matches ) ) {
-			$url = trim( $matches[1] );
-			// Validate the extracted URL.
-			if ( filter_var( $url, FILTER_VALIDATE_URL ) && wp_http_validate_url( $url ) ) {
-				return $url;
-			}
-		}
-
-		return '';
+		// If not in attributes, extract from block content.
+		return $this->extract_url_from_content( $block_content );
 	}

 	/**
@@ -186,7 +298,17 @@ class Embed extends Abstract_Block_Renderer {
 			return $attr['label'];
 		}

-		// Use default label based on provider.
+		// Get translated label for the provider.
+		return $this->get_translated_provider_label( $provider );
+	}
+
+	/**
+	 * Get translated label for a provider.
+	 *
+	 * @param string $provider Provider name.
+	 * @return string Translated label for the provider.
+	 */
+	private function get_translated_provider_label( string $provider ): string {
 		switch ( $provider ) {
 			case 'spotify':
 				return __( 'Listen on Spotify', 'woocommerce' );
@@ -198,13 +320,15 @@ class Embed extends Abstract_Block_Renderer {
 				return __( 'Listen on Mixcloud', 'woocommerce' );
 			case 'reverbnation':
 				return __( 'Listen on ReverbNation', 'woocommerce' );
+			case 'youtube':
+				return __( 'Watch on YouTube', 'woocommerce' );
 			default:
 				return __( 'Listen to the audio', 'woocommerce' );
 		}
 	}

 	/**
-	 * Render a simple link fallback for non-audio embeds.
+	 * Render a simple link fallback for non-supported embeds.
 	 *
 	 * @param array             $attr Block attributes.
 	 * @param string            $block_content Block content.
@@ -218,9 +342,21 @@ class Embed extends Abstract_Block_Renderer {

 		// If no URL in attributes, try to extract from block content.
 		if ( empty( $url ) ) {
-			// Look for any HTTP/HTTPS URL in the content with proper boundaries.
-			if ( preg_match( '/(?<![a-zA-Z0-9.-])https?:\/\/[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}[a-zA-Z0-9\/?=&%-]*(?![a-zA-Z0-9.-])/', $block_content, $matches ) ) {
-				$url = $matches[0];
+			// First try the standard wrapper div extraction.
+			$url = $this->extract_url_from_content( $block_content );
+
+			// If still no URL, try to find any HTTP/HTTPS URL in the entire content.
+			if ( empty( $url ) ) {
+				$dom_helper   = new Dom_Document_Helper( $block_content );
+				$body_element = $dom_helper->find_element( 'body' );
+				if ( $body_element ) {
+					$text_content = $body_element->textContent; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
+
+					// Look for HTTP/HTTPS URLs in the text content.
+					if ( preg_match( '/(?<![a-zA-Z0-9.-])https?:\/\/[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}[a-zA-Z0-9\/?=&%-]*(?![a-zA-Z0-9.-])/', $text_content, $matches ) ) {
+						$url = $matches[0];
+					}
+				}
 			}
 		}

@@ -230,7 +366,7 @@ class Embed extends Abstract_Block_Renderer {
 		}

 		// Validate URL with both filter_var and wp_http_validate_url.
-		if ( empty( $url ) || ! filter_var( $url, FILTER_VALIDATE_URL ) || ! wp_http_validate_url( $url ) ) {
+		if ( ! $this->is_valid_url( $url ) ) {
 			return '';
 		}

@@ -279,19 +415,105 @@ class Embed extends Abstract_Block_Renderer {
 	 * @return string Base URL for the provider or empty string.
 	 */
 	private function get_provider_base_url( string $provider ): string {
-		switch ( $provider ) {
-			case 'spotify':
-				return 'https://open.spotify.com/';
-			case 'soundcloud':
-				return 'https://soundcloud.com/';
-			case 'pocket-casts':
-				return 'https://pca.st/';
-			case 'mixcloud':
-				return 'https://www.mixcloud.com/';
-			case 'reverbnation':
-				return 'https://www.reverbnation.com/';
-			default:
-				return '';
+		$all_providers = $this->get_all_provider_configs();
+		return $all_providers[ $provider ]['base_url'] ?? '';
+	}
+
+	/**
+	 * Check if a provider is a video provider.
+	 *
+	 * @param string $provider Provider name.
+	 * @return bool True if video provider.
+	 */
+	private function is_video_provider( string $provider ): bool {
+		return array_key_exists( $provider, self::VIDEO_PROVIDERS );
+	}
+
+	/**
+	 * Render a video embed using the Video renderer.
+	 *
+	 * @param string            $url URL of the video.
+	 * @param string            $provider Provider name.
+	 * @param array             $parsed_block Parsed block.
+	 * @param Rendering_Context $rendering_context Rendering context.
+	 * @param string            $block_content Original block content.
+	 * @return string Rendered video embed or fallback.
+	 */
+	private function render_video_embed( string $url, string $provider, array $parsed_block, Rendering_Context $rendering_context, string $block_content ): string {
+		// Try to get video thumbnail URL.
+		$poster_url = $this->get_video_thumbnail_url( $url, $provider );
+
+		// If no poster available, fall back to a simple link.
+		if ( empty( $poster_url ) ) {
+			$fallback_attr = $this->create_fallback_attributes( $url, $url );
+			return $this->render_link_fallback( $fallback_attr, $block_content, $parsed_block, $rendering_context );
 		}
+
+		// Create a mock video block structure to reuse the Video renderer.
+		$mock_video_block = array(
+			'blockName' => 'core/video',
+			'attrs'     => array(
+				'poster' => $poster_url,
+			),
+			'innerHTML' => '<figure class="wp-block-video wp-block-embed is-type-video is-provider-' . esc_attr( $provider ) . '"><div class="wp-block-embed__wrapper">' . esc_url( $url ) . '</div></figure>',
+		);
+
+		// Copy email attributes to the mock block.
+		if ( isset( $parsed_block['email_attrs'] ) ) {
+			$mock_video_block['email_attrs'] = $parsed_block['email_attrs'];
+		}
+
+		// Use the Video renderer to render the video provider embed.
+		$video_renderer = new Video();
+		$video_result   = $video_renderer->render( $mock_video_block['innerHTML'], $mock_video_block, $rendering_context );
+
+		// If video rendering fails, fall back to a simple link.
+		if ( empty( $video_result ) ) {
+			$fallback_attr = $this->create_fallback_attributes( $url, $url );
+			return $this->render_link_fallback( $fallback_attr, $block_content, $parsed_block, $rendering_context );
+		}
+
+		return $video_result;
+	}
+
+	/**
+	 * Get video thumbnail URL for supported providers.
+	 *
+	 * @param string $url Video URL.
+	 * @param string $provider Provider name.
+	 * @return string Thumbnail URL or empty string.
+	 */
+	private function get_video_thumbnail_url( string $url, string $provider ): string {
+		// Currently only YouTube supports thumbnail extraction.
+		if ( 'youtube' === $provider ) {
+			return $this->get_youtube_thumbnail( $url );
+		}
+
+		// For other providers, we don't have thumbnail extraction implemented.
+		// Return empty to trigger link fallback.
+		return '';
+	}
+
+	/**
+	 * Extract YouTube video thumbnail URL.
+	 *
+	 * @param string $url YouTube video URL.
+	 * @return string Thumbnail URL or empty string.
+	 */
+	private function get_youtube_thumbnail( string $url ): string {
+		// Extract video ID from various YouTube URL formats.
+		$video_id = '';
+
+		if ( preg_match( '/(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]+)/', $url, $matches ) ) {
+			$video_id = $matches[1];
+		}
+
+		if ( empty( $video_id ) ) {
+			return '';
+		}
+
+		// Return YouTube thumbnail URL.
+		// Using 0.jpg format as shown in the example.
+		return 'https://img.youtube.com/vi/' . $video_id . '/0.jpg';
 	}
 }
diff --git a/packages/php/email-editor/src/Integrations/Core/Renderer/Blocks/class-video.php b/packages/php/email-editor/src/Integrations/Core/Renderer/Blocks/class-video.php
new file mode 100644
index 0000000000..06312ff744
--- /dev/null
+++ b/packages/php/email-editor/src/Integrations/Core/Renderer/Blocks/class-video.php
@@ -0,0 +1,208 @@
+<?php
+/**
+ * This file is part of the WooCommerce Email Editor package
+ *
+ * @package Automattic\WooCommerce\EmailEditor
+ */
+
+declare( strict_types = 1 );
+namespace Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks;
+
+use Automattic\WooCommerce\EmailEditor\Engine\Renderer\ContentRenderer\Rendering_Context;
+use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Dom_Document_Helper;
+
+/**
+ * Video block renderer.
+ * This renderer handles core/video blocks by reusing the cover block renderer
+ * to show a thumbnail with a play button overlay.
+ */
+class Video extends Cover {
+	/**
+	 * Renders the video block content by transforming it into a cover block structure.
+	 * Shows the video poster/thumbnail with a play button overlay using the parent cover renderer.
+	 *
+	 * @param string            $block_content Block content.
+	 * @param array             $parsed_block Parsed block.
+	 * @param Rendering_Context $rendering_context Rendering context.
+	 * @return string
+	 */
+	protected function render_content( string $block_content, array $parsed_block, Rendering_Context $rendering_context ): string {
+		$block_attrs = $parsed_block['attrs'] ?? array();
+
+		// Extract poster URL from video attributes.
+		$poster_url = $this->extract_poster_url( $block_attrs, $block_content );
+
+		// If no poster image, return empty content.
+		if ( empty( $poster_url ) ) {
+			return '';
+		}
+
+		// Transform video block into cover block structure and delegate to parent.
+		$cover_block = $this->transform_to_cover_block( $parsed_block, $poster_url );
+		return parent::render_content( $block_content, $cover_block, $rendering_context );
+	}
+
+	/**
+	 * Extract poster URL from block attributes.
+	 *
+	 * @param array  $block_attrs Block attributes.
+	 * @param string $block_content Original block content (unused, kept for consistency).
+	 * @return string Poster URL or empty string.
+	 */
+	private function extract_poster_url( array $block_attrs, string $block_content ): string {
+		// Check for poster attribute.
+		if ( ! empty( $block_attrs['poster'] ) ) {
+			return esc_url( $block_attrs['poster'] );
+		}
+
+		return '';
+	}
+
+	/**
+	 * Extract video URL from block content.
+	 *
+	 * @param string $block_content Block content HTML.
+	 * @return string Video URL or empty string.
+	 */
+	private function extract_video_url( string $block_content ): string {
+		// Use Dom_Document_Helper for robust HTML parsing.
+		$dom_helper = new Dom_Document_Helper( $block_content );
+
+		// Find the wp-block-embed__wrapper div.
+		$wrapper_element = $dom_helper->find_element( 'div' );
+		if ( ! $wrapper_element ) {
+			return '';
+		}
+
+		// Check if this div has the correct class.
+		$class_attr = $dom_helper->get_attribute_value( $wrapper_element, 'class' );
+		if ( strpos( $class_attr, 'wp-block-embed__wrapper' ) === false ) {
+			return '';
+		}
+
+		// Get the inner HTML content from the wrapper div.
+		$inner_html = $dom_helper->get_element_inner_html( $wrapper_element );
+
+		// Look for HTTP/HTTPS URLs in the inner HTML content.
+		if ( preg_match( '/(?<![a-zA-Z0-9.-])https?:\/\/[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}[a-zA-Z0-9\/?=&%-]*(?![a-zA-Z0-9.-])/', $inner_html, $matches ) ) {
+			$url = $matches[0];
+
+			// Decode HTML entities and validate URL.
+			$url = html_entity_decode( $url, ENT_QUOTES | ENT_HTML5, 'UTF-8' );
+
+			// Validate the URL.
+			if ( filter_var( $url, FILTER_VALIDATE_URL ) && wp_http_validate_url( $url ) ) {
+				return $url;
+			}
+		}
+
+		return '';
+	}
+
+	/**
+	 * Transform a video block into a cover block structure.
+	 *
+	 * @param array  $video_block Original video block.
+	 * @param string $poster_url Poster URL to use as background.
+	 * @return array Cover block structure.
+	 */
+	private function transform_to_cover_block( array $video_block, string $poster_url ): array {
+		$block_attrs   = $video_block['attrs'] ?? array();
+		$block_content = $video_block['innerHTML'] ?? '';
+
+		// Extract video URL from block content, fall back to post URL.
+		// Priority: 1) Video URL (if found), 2) Post permalink (fallback).
+		$video_url = $this->extract_video_url( $block_content );
+		$link_url  = ! empty( $video_url ) ? $video_url : $this->get_current_post_url();
+
+		return array(
+			'blockName'   => 'core/cover',
+			'attrs'       => array(
+				'url'       => $poster_url,
+				'minHeight' => '390px', // Custom attribute for video blocks.
+			),
+			'innerBlocks' => array(
+				array(
+					'blockName'    => 'core/html',
+					'attrs'        => array(),
+					'innerBlocks'  => array(),
+					'innerHTML'    => $this->create_play_button_html( $link_url ),
+					'innerContent' => array( $this->create_play_button_html( $link_url ) ),
+				),
+			),
+			'innerHTML'   => $block_content,
+		);
+	}
+
+	/**
+	 * Create the play button HTML with optional link.
+	 *
+	 * @param string $link_url Optional URL to link to.
+	 * @return string Play button HTML.
+	 */
+	private function create_play_button_html( string $link_url = '' ): string {
+		$play_icon_url = $this->get_play_icon_url();
+
+		$play_button = sprintf(
+			'<img src="%s" alt="%s" style="width: 48px; height: 48px; display: inline-block;" />',
+			esc_url( $play_icon_url ),
+			// translators: Alt text for video play button icon.
+			esc_attr( __( 'Play', 'woocommerce' ) )
+		);
+
+		// Wrap the play button in a link if URL is provided.
+		if ( ! empty( $link_url ) ) {
+			$play_button = sprintf(
+				'<a href="%s" target="_blank" rel="noopener noreferrer nofollow" style="display: inline-block; text-decoration: none;">%s</a>',
+				esc_url( $link_url ),
+				$play_button
+			);
+		}
+
+		return sprintf(
+			'<p style="text-align: center;">%s</p>',
+			$play_button
+		);
+	}
+
+	/**
+	 * Get the URL for the play button icon.
+	 *
+	 * @return string Play button icon URL.
+	 */
+	private function get_play_icon_url(): string {
+		$file_name = '/icons/video/play2x.png';
+		return plugins_url( $file_name, __FILE__ );
+	}
+
+	/**
+	 * Get the current post permalink with security validation.
+	 *
+	 * @return string Post permalink or empty string if invalid.
+	 */
+	private function get_current_post_url(): string {
+		global $post;
+
+		if ( ! $post instanceof \WP_Post ) {
+			return '';
+		}
+
+		$permalink = get_permalink( $post->ID );
+		if ( empty( $permalink ) ) {
+			return '';
+		}
+
+		// Validate URL type and format (following audio block pattern).
+		if ( strpos( $permalink, 'https://' ) !== 0 && strpos( $permalink, 'http://' ) !== 0 ) {
+			// Reject non-HTTP protocols for security.
+			return '';
+		}
+
+		// For all HTTP(S) URLs, validate with wp_http_validate_url.
+		if ( ! wp_http_validate_url( $permalink ) ) {
+			return '';
+		}
+
+		return $permalink;
+	}
+}
diff --git a/packages/php/email-editor/src/Integrations/Core/Renderer/Blocks/icons/video/play2x.png b/packages/php/email-editor/src/Integrations/Core/Renderer/Blocks/icons/video/play2x.png
new file mode 100644
index 0000000000..bff0e047af
Binary files /dev/null and b/packages/php/email-editor/src/Integrations/Core/Renderer/Blocks/icons/video/play2x.png differ
diff --git a/packages/php/email-editor/src/Integrations/Core/class-initializer.php b/packages/php/email-editor/src/Integrations/Core/class-initializer.php
index 42117ebd44..774cebe927 100644
--- a/packages/php/email-editor/src/Integrations/Core/class-initializer.php
+++ b/packages/php/email-editor/src/Integrations/Core/class-initializer.php
@@ -26,6 +26,7 @@ use Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks\List_It
 use Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks\Media_Text;
 use Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks\Cover;
 use Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks\Quote;
+use Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks\Video;
 use Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks\Social_Link;
 use Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks\Social_Links;
 use Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks\Table;
@@ -72,6 +73,7 @@ class Initializer {
 		'core/audio',
 		'core/embed',
 		'core/cover',
+		'core/video',
 	);

 	/**
@@ -233,6 +235,9 @@ class Initializer {
 			case 'core/cover':
 				$renderer = new Cover();
 				break;
+			case 'core/video':
+				$renderer = new Video();
+				break;
 			default:
 				$renderer = new Fallback();
 				break;
diff --git a/packages/php/email-editor/tests/integration/Integrations/Core/Renderer/Blocks/Embed_Test.php b/packages/php/email-editor/tests/integration/Integrations/Core/Renderer/Blocks/Embed_Test.php
index 4dde2cb0e9..1425d8f62e 100644
--- a/packages/php/email-editor/tests/integration/Integrations/Core/Renderer/Blocks/Embed_Test.php
+++ b/packages/php/email-editor/tests/integration/Integrations/Core/Renderer/Blocks/Embed_Test.php
@@ -299,26 +299,47 @@ class Embed_Test extends \Email_Editor_Integration_Test_Case {
 	 * Test that non-audio embeds render as link fallback
 	 */
 	public function test_renders_non_audio_embeds_as_link_fallback(): void {
-		$rendered = $this->embed_renderer->render( $this->parsed_youtube_embed['innerHTML'], $this->parsed_youtube_embed, $this->rendering_context );
+		// Use a non-supported embed provider for this test.
+		$parsed_unsupported_embed = array(
+			'blockName' => 'core/embed',
+			'attrs'     => array(
+				'url'              => 'https://example.com/embed',
+				'type'             => 'rich',
+				'providerNameSlug' => 'example',
+				'responsive'       => true,
+			),
+			'innerHTML' => '<figure class="wp-block-embed is-type-rich is-provider-example"><div class="wp-block-embed__wrapper">https://example.com/embed</div></figure>',
+		);
+
+		$rendered = $this->embed_renderer->render( $parsed_unsupported_embed['innerHTML'], $parsed_unsupported_embed, $this->rendering_context );

 		// Check that the rendered content contains a link.
-		$this->assertStringContainsString( '<a href="https://www.youtube.com/watch?v=dQw4w9WgXcQ"', $rendered );
+		$this->assertStringContainsString( '<a href="https://example.com/embed"', $rendered );
 		$this->assertStringContainsString( 'target="_blank"', $rendered );
 		$this->assertStringContainsString( 'rel="noopener nofollow"', $rendered );
-		$this->assertStringContainsString( 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', $rendered );
+		$this->assertStringContainsString( 'https://example.com/embed', $rendered );
 	}

 	/**
 	 * Test that link fallback uses custom label when provided
 	 */
 	public function test_link_fallback_uses_custom_label(): void {
-		$parsed_youtube_with_label                   = $this->parsed_youtube_embed;
-		$parsed_youtube_with_label['attrs']['label'] = 'Watch this video';
+		$parsed_unsupported_with_label = array(
+			'blockName' => 'core/embed',
+			'attrs'     => array(
+				'url'              => 'https://example.com/embed',
+				'type'             => 'rich',
+				'providerNameSlug' => 'example',
+				'responsive'       => true,
+				'label'            => 'Watch this video',
+			),
+			'innerHTML' => '<figure class="wp-block-embed is-type-rich is-provider-example"><div class="wp-block-embed__wrapper">https://example.com/embed</div></figure>',
+		);

-		$rendered = $this->embed_renderer->render( $this->parsed_youtube_embed['innerHTML'], $parsed_youtube_with_label, $this->rendering_context );
+		$rendered = $this->embed_renderer->render( $parsed_unsupported_with_label['innerHTML'], $parsed_unsupported_with_label, $this->rendering_context );

 		$this->assertStringContainsString( 'Watch this video', $rendered );
-		$this->assertStringContainsString( '<a href="https://www.youtube.com/watch?v=dQw4w9WgXcQ"', $rendered );
+		$this->assertStringContainsString( '<a href="https://example.com/embed"', $rendered );
 		// The link text should be the custom label, not the URL.
 		$this->assertStringContainsString( '>Watch this video</a>', $rendered );
 	}
@@ -327,19 +348,26 @@ class Embed_Test extends \Email_Editor_Integration_Test_Case {
 	 * Test that link fallback extracts URL from content when not in attributes
 	 */
 	public function test_link_fallback_extracts_url_from_content(): void {
-		$parsed_youtube_no_url_attr = $this->parsed_youtube_embed;
-		unset( $parsed_youtube_no_url_attr['attrs']['url'] );
+		$parsed_unsupported_no_url_attr = array(
+			'blockName' => 'core/embed',
+			'attrs'     => array(
+				'type'             => 'rich',
+				'providerNameSlug' => 'example',
+				'responsive'       => true,
+			),
+			'innerHTML' => '<figure class="wp-block-embed is-type-rich is-provider-example"><div class="wp-block-embed__wrapper">https://example.com/embed</div></figure>',
+		);

-		$rendered = $this->embed_renderer->render( $this->parsed_youtube_embed['innerHTML'], $parsed_youtube_no_url_attr, $this->rendering_context );
+		$rendered = $this->embed_renderer->render( $parsed_unsupported_no_url_attr['innerHTML'], $parsed_unsupported_no_url_attr, $this->rendering_context );

-		$this->assertStringContainsString( '<a href="https://www.youtube.com/watch?v=dQw4w9WgXcQ"', $rendered );
-		$this->assertStringContainsString( 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', $rendered );
+		$this->assertStringContainsString( '<a href="https://example.com/embed"', $rendered );
+		$this->assertStringContainsString( 'https://example.com/embed', $rendered );
 	}

 	/**
-	 * Test that link fallback returns empty when no valid URL is found
+	 * Test that link fallback returns base URL when no valid URL is found but provider is known
 	 */
-	public function test_link_fallback_returns_empty_when_no_valid_url(): void {
+	public function test_link_fallback_returns_base_url_when_no_valid_url(): void {
 		$parsed_embed_no_url = array(
 			'blockName' => 'core/embed',
 			'attrs'     => array(
@@ -350,7 +378,11 @@ class Embed_Test extends \Email_Editor_Integration_Test_Case {

 		$rendered = $this->embed_renderer->render( $parsed_embed_no_url['innerHTML'], $parsed_embed_no_url, $this->rendering_context );

-		$this->assertEmpty( $rendered );
+		// Should return graceful fallback link since provider is detected but no URL is available.
+		$this->assertStringContainsString( '<a href="https://www.youtube.com/"', $rendered );
+		$this->assertStringContainsString( 'Watch on YouTube', $rendered );
+		$this->assertStringContainsString( 'target="_blank"', $rendered );
+		$this->assertStringContainsString( 'rel="noopener nofollow"', $rendered );
 	}

 	/**
@@ -396,4 +428,155 @@ class Embed_Test extends \Email_Editor_Integration_Test_Case {
 		$this->assertStringContainsString( 'Listen on ReverbNation', $rendered );
 		$this->assertStringContainsString( 'https://reverbnation.com/artist/example-song', $rendered );
 	}
+
+	/**
+	 * Test that YouTube embed renders as video player
+	 */
+	public function test_renders_youtube_embed_as_video_player(): void {
+		$rendered = $this->embed_renderer->render( $this->parsed_youtube_embed['innerHTML'], $this->parsed_youtube_embed, $this->rendering_context );
+
+		// Check that the rendered content contains YouTube video elements.
+		$this->assertStringContainsString( 'https://img.youtube.com/vi/dQw4w9WgXcQ/0.jpg', $rendered );
+		$this->assertStringContainsString( 'play2x.png', $rendered );
+		$this->assertStringContainsString( 'background-image:url(&quot;https://img.youtube.com/vi/dQw4w9WgXcQ/0.jpg&quot;)', $rendered );
+		$this->assertStringContainsString( 'background-size:cover', $rendered );
+		$this->assertStringContainsString( 'min-height:390px', $rendered );
+	}
+
+	/**
+	 * Test that YouTube embed uses custom label when provided
+	 */
+	public function test_youtube_embed_uses_custom_label_when_provided(): void {
+		$parsed_youtube_custom_label                   = $this->parsed_youtube_embed;
+		$parsed_youtube_custom_label['attrs']['label'] = 'Watch this video';
+
+		$rendered = $this->embed_renderer->render( $this->parsed_youtube_embed['innerHTML'], $parsed_youtube_custom_label, $this->rendering_context );
+
+		// Custom labels are not used in video rendering - the play button is always "Play".
+		$this->assertStringContainsString( 'alt="Play"', $rendered );
+	}
+
+	/**
+	 * Test that YouTube embed handles email attributes for spacing
+	 */
+	public function test_youtube_embed_handles_email_attributes_for_spacing(): void {
+		$parsed_youtube_with_spacing                = $this->parsed_youtube_embed;
+		$parsed_youtube_with_spacing['email_attrs'] = array(
+			'margin' => '20px 0',
+		);
+
+		$rendered = $this->embed_renderer->render( $this->parsed_youtube_embed['innerHTML'], $parsed_youtube_with_spacing, $this->rendering_context );
+
+		// Email attributes are handled by the cover block renderer.
+		$this->assertStringContainsString( 'https://img.youtube.com/vi/dQw4w9WgXcQ/0.jpg', $rendered );
+		$this->assertStringContainsString( 'play2x.png', $rendered );
+	}
+
+	/**
+	 * Test that YouTube embed includes proper security attributes
+	 */
+	public function test_youtube_embed_includes_proper_security_attributes(): void {
+		$rendered = $this->embed_renderer->render( $this->parsed_youtube_embed['innerHTML'], $this->parsed_youtube_embed, $this->rendering_context );
+
+		// The play button may or may not be wrapped in a link depending on post context.
+		// In test environment, there may not be a valid post URL, so the play button is just an image.
+		$this->assertStringContainsString( 'alt="Play"', $rendered );
+		$this->assertStringContainsString( 'play2x.png', $rendered );
+	}
+
+	/**
+	 * Test that YouTube embed includes proper accessibility attributes
+	 */
+	public function test_youtube_embed_includes_proper_accessibility_attributes(): void {
+		$rendered = $this->embed_renderer->render( $this->parsed_youtube_embed['innerHTML'], $this->parsed_youtube_embed, $this->rendering_context );
+
+		$this->assertStringContainsString( 'alt="Play"', $rendered );
+	}
+
+	/**
+	 * Test that YouTube embed detects YouTube by providerNameSlug
+	 */
+	public function test_youtube_embed_detects_youtube_by_provider_name_slug(): void {
+		$parsed_youtube_by_slug = $this->parsed_youtube_embed;
+		unset( $parsed_youtube_by_slug['attrs']['url'] );
+		$parsed_youtube_by_slug['innerHTML'] = '<figure class="wp-block-embed is-type-video is-provider-youtube"><div class="wp-block-embed__wrapper">Some content</div></figure>';
+
+		$rendered = $this->embed_renderer->render( $parsed_youtube_by_slug['innerHTML'], $parsed_youtube_by_slug, $this->rendering_context );
+
+		// Should return graceful fallback link since provider is detected but no URL is available for thumbnail extraction.
+		$this->assertStringContainsString( '<a href="https://www.youtube.com/"', $rendered );
+		$this->assertStringContainsString( 'Watch on YouTube', $rendered );
+		$this->assertStringContainsString( 'target="_blank"', $rendered );
+		$this->assertStringContainsString( 'rel="noopener nofollow"', $rendered );
+	}
+
+	/**
+	 * Test that YouTube embed detects YouTube by URL in attributes
+	 */
+	public function test_youtube_embed_detects_youtube_by_url_in_attributes(): void {
+		$parsed_youtube_by_url = $this->parsed_youtube_embed;
+		unset( $parsed_youtube_by_url['attrs']['providerNameSlug'] );
+
+		$rendered = $this->embed_renderer->render( $this->parsed_youtube_embed['innerHTML'], $parsed_youtube_by_url, $this->rendering_context );
+
+		$this->assertStringContainsString( 'https://img.youtube.com/vi/dQw4w9WgXcQ/0.jpg', $rendered );
+		$this->assertStringContainsString( 'play2x.png', $rendered );
+	}
+
+	/**
+	 * Test that YouTube embed detects YouTube by URL in content
+	 */
+	public function test_youtube_embed_detects_youtube_by_url_in_content(): void {
+		$parsed_youtube_by_content = $this->parsed_youtube_embed;
+		unset( $parsed_youtube_by_content['attrs']['providerNameSlug'] );
+		unset( $parsed_youtube_by_content['attrs']['url'] );
+
+		$rendered = $this->embed_renderer->render( $this->parsed_youtube_embed['innerHTML'], $parsed_youtube_by_content, $this->rendering_context );
+
+		$this->assertStringContainsString( 'https://img.youtube.com/vi/dQw4w9WgXcQ/0.jpg', $rendered );
+		$this->assertStringContainsString( 'play2x.png', $rendered );
+	}
+
+	/**
+	 * Test that YouTube embed handles youtu.be URLs
+	 */
+	public function test_youtube_embed_handles_youtu_be_urls(): void {
+		$parsed_youtube_short_url = array(
+			'blockName' => 'core/embed',
+			'attrs'     => array(
+				'url'              => 'https://youtu.be/dQw4w9WgXcQ',
+				'type'             => 'video',
+				'providerNameSlug' => 'youtube',
+				'responsive'       => true,
+			),
+			'innerHTML' => '<figure class="wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube"><div class="wp-block-embed__wrapper">https://youtu.be/dQw4w9WgXcQ</div></figure>',
+		);
+
+		$rendered = $this->embed_renderer->render( $parsed_youtube_short_url['innerHTML'], $parsed_youtube_short_url, $this->rendering_context );
+
+		$this->assertStringContainsString( 'https://img.youtube.com/vi/dQw4w9WgXcQ/0.jpg', $rendered );
+		$this->assertStringContainsString( 'play2x.png', $rendered );
+	}
+
+	/**
+	 * Test that YouTube embed falls back to link when thumbnail extraction fails
+	 */
+	public function test_youtube_embed_falls_back_to_link_when_thumbnail_fails(): void {
+		$parsed_youtube_invalid = array(
+			'blockName' => 'core/embed',
+			'attrs'     => array(
+				'url'              => 'https://www.youtube.com/watch?v=invalid',
+				'type'             => 'video',
+				'providerNameSlug' => 'youtube',
+				'responsive'       => true,
+			),
+			'innerHTML' => '<figure class="wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube"><div class="wp-block-embed__wrapper">https://www.youtube.com/watch?v=invalid</div></figure>',
+		);
+
+		$rendered = $this->embed_renderer->render( $parsed_youtube_invalid['innerHTML'], $parsed_youtube_invalid, $this->rendering_context );
+
+		// Should still render as video block even with invalid video ID (the thumbnail URL will be generated).
+		$this->assertStringContainsString( 'https://img.youtube.com/vi/invalid/0.jpg', $rendered );
+		$this->assertStringContainsString( 'play2x.png', $rendered );
+	}
 }
diff --git a/packages/php/email-editor/tests/integration/Integrations/Core/Renderer/Blocks/Video_Test.php b/packages/php/email-editor/tests/integration/Integrations/Core/Renderer/Blocks/Video_Test.php
new file mode 100644
index 0000000000..1094f97dba
--- /dev/null
+++ b/packages/php/email-editor/tests/integration/Integrations/Core/Renderer/Blocks/Video_Test.php
@@ -0,0 +1,236 @@
+<?php
+/**
+ * This file is part of the WooCommerce Email Editor package
+ *
+ * @package Automattic\WooCommerce\EmailEditor
+ */
+
+declare( strict_types = 1 );
+namespace Automattic\WooCommerce\EmailEditor\Tests\Integration\Integrations\Core\Renderer\Blocks;
+
+use Automattic\WooCommerce\EmailEditor\Engine\Email_Editor;
+use Automattic\WooCommerce\EmailEditor\Engine\Renderer\ContentRenderer\Rendering_Context;
+use Automattic\WooCommerce\EmailEditor\Engine\Theme_Controller;
+use Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks\Video;
+
+/**
+ * Integration test for Video class
+ */
+class Video_Test extends \Email_Editor_Integration_Test_Case {
+	/**
+	 * Video renderer instance
+	 *
+	 * @var Video
+	 */
+	private $video_renderer;
+
+	/**
+	 * Basic video block configuration
+	 *
+	 * @var array
+	 */
+	private $parsed_video = array(
+		'blockName' => 'core/video',
+		'attrs'     => array(
+			'poster' => 'https://example.com/poster.jpg',
+			'title'  => 'Sample Video',
+		),
+		'innerHTML' => '<figure class="wp-block-video"><video controls poster="https://example.com/poster.jpg"><source src="https://example.com/video.mp4" type="video/mp4" /></video></figure>',
+	);
+
+	/**
+	 * Video block with different poster URL for variety
+	 *
+	 * @var array
+	 */
+	private $parsed_video_alt = array(
+		'blockName' => 'core/video',
+		'attrs'     => array(
+			'poster' => 'https://example.com/another-poster.jpg',
+			'title'  => 'Another Video',
+		),
+		'innerHTML' => '<figure class="wp-block-video"><video controls poster="https://example.com/another-poster.jpg"><source src="https://example.com/video2.mp4" type="video/mp4" /></video></figure>',
+	);
+
+	/**
+	 * Rendering context instance.
+	 *
+	 * @var Rendering_Context
+	 */
+	private $rendering_context;
+
+	/**
+	 * Set up before each test
+	 */
+	public function setUp(): void {
+		parent::setUp();
+		$this->di_container->get( Email_Editor::class )->initialize();
+		$this->video_renderer    = new Video();
+		$theme_controller        = $this->di_container->get( Theme_Controller::class );
+		$this->rendering_context = new Rendering_Context( $theme_controller->get_theme() );
+	}
+
+	/**
+	 * Test it renders video content with poster and play button
+	 */
+	public function testItRendersVideoContentWithPosterAndPlayButton(): void {
+		$rendered = $this->video_renderer->render( '', $this->parsed_video, $this->rendering_context );
+
+		// Should contain cover block elements (since we're reusing cover renderer).
+		$this->assertStringContainsString( 'email-block-cover', $rendered );
+
+		// Should contain the poster image as background.
+		$this->assertStringContainsString( 'background-image:url(&quot;https://example.com/poster.jpg&quot;)', $rendered );
+
+		// Should contain play button.
+		$this->assertStringContainsString( 'play2x.png', $rendered );
+		$this->assertStringContainsString( 'alt="Play"', $rendered );
+	}
+
+	/**
+	 * Test it extracts poster URL from attributes
+	 */
+	public function testItExtractsPosterUrlFromAttributes(): void {
+		$rendered = $this->video_renderer->render( '', $this->parsed_video, $this->rendering_context );
+		$this->assertStringContainsString( 'background-image:url(&quot;https://example.com/poster.jpg&quot;)', $rendered );
+	}
+
+	/**
+	 * Test it works with different poster URLs
+	 */
+	public function testItWorksWithDifferentPosterUrls(): void {
+		$rendered = $this->video_renderer->render( '', $this->parsed_video_alt, $this->rendering_context );
+		$this->assertStringContainsString( 'background-image:url(&quot;https://example.com/another-poster.jpg&quot;)', $rendered );
+		$this->assertStringContainsString( 'email-block-cover', $rendered );
+	}
+
+	/**
+	 * Test it extracts video title from attributes
+	 */
+	public function testItExtractsVideoTitleFromAttributes(): void {
+		$rendered = $this->video_renderer->render( '', $this->parsed_video, $this->rendering_context );
+
+		// The title should be used as alt text for the background image.
+		// Note: The cover renderer handles this, so we check for table structure.
+		$this->assertStringContainsString( 'email-block-cover', $rendered );
+	}
+
+	/**
+	 * Test it handles video without poster (returns empty)
+	 */
+	public function testItHandlesVideoWithoutPoster(): void {
+		$parsed_video              = $this->parsed_video;
+		$parsed_video['attrs']     = array();
+		$parsed_video['innerHTML'] = '<figure class="wp-block-video"><video controls><source src="https://example.com/video.mp4" type="video/mp4" /></video></figure>';
+
+		$rendered = $this->video_renderer->render( '', $parsed_video, $this->rendering_context );
+
+		// Should return empty content when no poster is available.
+		$this->assertStringNotContainsString( 'email-block-cover', $rendered );
+	}
+
+	/**
+	 * Test it provides default title when none specified
+	 */
+	public function testItProvidesDefaultTitleWhenNoneSpecified(): void {
+		$parsed_video          = $this->parsed_video;
+		$parsed_video['attrs'] = array(
+			'poster' => 'https://example.com/poster.jpg',
+		);
+
+		$rendered = $this->video_renderer->render( '', $parsed_video, $this->rendering_context );
+
+		// Should still render successfully with default title.
+		$this->assertStringContainsString( 'email-block-cover', $rendered );
+		$this->assertStringContainsString( 'background-image', $rendered );
+	}
+
+	/**
+	 * Test it creates play button with correct styling
+	 */
+	public function testItCreatesPlayButtonWithCorrectStyling(): void {
+		$rendered = $this->video_renderer->render( '', $this->parsed_video, $this->rendering_context );
+
+		// Should contain play button with proper styling.
+		$this->assertStringContainsString( 'width: 48px; height: 48px', $rendered );
+		$this->assertStringContainsString( 'display: inline-block', $rendered );
+		$this->assertStringContainsString( 'text-align: center', $rendered );
+	}
+
+	/**
+	 * Test it handles different video formats correctly
+	 */
+	public function testItHandlesDifferentVideoFormatsCorrectly(): void {
+		$rendered = $this->video_renderer->render( '', $this->parsed_video_alt, $this->rendering_context );
+
+		// Should render properly for any video with poster.
+		$this->assertStringContainsString( 'email-block-cover', $rendered );
+		$this->assertStringContainsString( 'background-image', $rendered );
+		$this->assertStringContainsString( 'play2x.png', $rendered );
+	}
+
+	/**
+	 * Test it renders with custom video HTML structure
+	 */
+	public function testItRendersWithCustomVideoHtmlStructure(): void {
+		$parsed_video              = $this->parsed_video;
+		$parsed_video['innerHTML'] = '<figure class="wp-block-video custom-class another-class"><video controls poster="https://example.com/poster.jpg"></video></figure>';
+
+		$rendered = $this->video_renderer->render( '', $parsed_video, $this->rendering_context );
+
+		// Should render successfully regardless of HTML structure (classes don't matter in email).
+		$this->assertStringContainsString( 'email-block-cover', $rendered );
+		$this->assertStringContainsString( 'background-image', $rendered );
+		$this->assertStringContainsString( 'play2x.png', $rendered );
+	}
+
+	/**
+	 * Test it creates email-compatible table structure
+	 */
+	public function testItCreatesEmailCompatibleTableStructure(): void {
+		$rendered = $this->video_renderer->render( '', $this->parsed_video, $this->rendering_context );
+
+		// Should use table-based layout for email compatibility.
+		$this->assertStringContainsString( '<table', $rendered );
+		$this->assertStringContainsString( 'width="100%"', $rendered );
+		$this->assertStringContainsString( 'border-collapse', $rendered );
+	}
+
+	/**
+	 * Test it returns empty when no poster available
+	 */
+	public function testItReturnsEmptyWhenNoPosterAvailable(): void {
+		$parsed_video              = $this->parsed_video;
+		$parsed_video['attrs']     = array(); // Remove poster from attrs.
+		$parsed_video['innerHTML'] = '<figure class="wp-block-video"><video controls><source src="https://example.com/video.mp4" type="video/mp4" /></video></figure>';
+
+		$rendered = $this->video_renderer->render( '', $parsed_video, $this->rendering_context );
+
+		// Should return empty content when no poster is available.
+		$this->assertStringNotContainsString( 'email-block-cover', $rendered );
+		$this->assertStringNotContainsString( 'background-image', $rendered );
+	}
+
+	/**
+	 * Test it delegates to cover renderer properly
+	 */
+	public function testItDelegatesToCoverRendererProperly(): void {
+		$rendered = $this->video_renderer->render( '', $this->parsed_video, $this->rendering_context );
+
+		// Should have all the characteristics of a cover block.
+		$this->assertStringContainsString( 'email-block-cover', $rendered );
+		$this->assertStringContainsString( 'background-size:cover', $rendered );
+		$this->assertStringContainsString( 'background-position:center', $rendered );
+		$this->assertStringContainsString( 'background-repeat:no-repeat', $rendered );
+	}
+
+	/**
+	 * Test it applies minimum height to video blocks
+	 */
+	public function testItAppliesMinimumHeightToVideoBlocks(): void {
+		$rendered = $this->video_renderer->render( '', $this->parsed_video, $this->rendering_context );
+
+		// Should apply minimum height for consistent video block appearance.
+		$this->assertStringContainsString( 'min-height:390px', $rendered );
+	}
+}