Skip to content

Commit 6fd0a19

Browse files
authored
Podcast player block: Add email rendering (#45003)
* Podcast player block: Add email rendering. * changelog * Fix class references in test file. * Fix covers function. * Change to a smaller pill shaped table. * Fix mock wrapper arg count.
1 parent 75e0d78 commit 6fd0a19

File tree

4 files changed

+348
-10
lines changed

4 files changed

+348
-10
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: minor
2+
Type: other
3+
4+
Podcast player block: Add email rendering.

projects/plugins/jetpack/extensions/blocks/podcast-player/podcast-player.php

Lines changed: 113 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,10 @@ function register_block() {
2626
Blocks::jetpack_register_block(
2727
__DIR__,
2828
array(
29-
'render_callback' => __NAMESPACE__ . '\render_block',
29+
'render_callback' => __NAMESPACE__ . '\render_block',
3030
// Since Gutenberg #31873.
31-
'style' => 'wp-mediaelement',
32-
31+
'style' => 'wp-mediaelement',
32+
'render_email_callback' => __NAMESPACE__ . '\render_email',
3333
)
3434
);
3535
}
@@ -293,3 +293,113 @@ function render( $name, $template_props = array(), $print = true ) {
293293
return $markup;
294294
}
295295
}
296+
297+
/**
298+
* Render podcast player block for email.
299+
*
300+
* @since $$next-version$$
301+
*
302+
* @param string $block_content The original block HTML content.
303+
* @param array $parsed_block The parsed block data including attributes.
304+
* @param object $rendering_context Email rendering context.
305+
*
306+
* @return string
307+
*/
308+
function render_email( $block_content, array $parsed_block, $rendering_context ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
309+
// Validate input parameters and required dependencies
310+
if ( ! isset( $parsed_block['attrs'] ) || ! is_array( $parsed_block['attrs'] ) ||
311+
! class_exists( '\Automattic\WooCommerce\EmailEditor\Integrations\Utils\Table_Wrapper_Helper' ) ) {
312+
return '';
313+
}
314+
315+
$attr = $parsed_block['attrs'];
316+
317+
// Check if we have a valid podcast URL
318+
if ( empty( $attr['url'] ) || ! wp_http_validate_url( $attr['url'] ) ) {
319+
return '';
320+
}
321+
322+
// Get spacing from email_attrs for better consistency with core blocks
323+
$email_attrs = $parsed_block['email_attrs'] ?? array();
324+
$table_margin_style = '';
325+
326+
if ( ! empty( $email_attrs ) && class_exists( '\WP_Style_Engine' ) ) {
327+
// Get margin for table styling
328+
$table_margin_style = \WP_Style_Engine::compile_css( array_intersect_key( $email_attrs, array_flip( array( 'margin' ) ) ), '' ) ?? '';
329+
330+
// Validate CSS output to prevent injection
331+
if ( ! empty( $table_margin_style ) && ! preg_match( '/^[a-zA-Z0-9\s:;()-]+$/', $table_margin_style ) ) {
332+
$table_margin_style = '';
333+
}
334+
}
335+
336+
$icon_image = 'https://s0.wp.com/i/emails/wpcom-notifications/audio-play.png';
337+
$label = __( 'Listen to the podcast', 'jetpack' );
338+
$audio_url = esc_url( $attr['url'] );
339+
340+
// Define pill-style colors and styling
341+
$background_color = '#f6f7f7';
342+
$border_color = '#AAA';
343+
$icon_size = '18px';
344+
$font_size = '14px';
345+
346+
// Generate the icon content
347+
$icon_content = sprintf(
348+
'<a href="%1$s" rel="noopener nofollow" target="_blank" style="padding: 0.25em; padding-left: 17px; display: inline-block; vertical-align: middle;"><img height="%2$s" src="%3$s" style="display:block;margin-right:0;vertical-align:middle;" width="%2$s" alt="%4$s"></a>',
349+
esc_url( $audio_url ),
350+
esc_attr( $icon_size ),
351+
esc_url( $icon_image ),
352+
// translators: %s is the podcast player icon.
353+
sprintf( __( '%s icon', 'jetpack' ), __( 'Podcast', 'jetpack' ) )
354+
);
355+
$icon_content = \Automattic\WooCommerce\EmailEditor\Integrations\Utils\Table_Wrapper_Helper::render_table_cell( $icon_content, array( 'style' => sprintf( 'vertical-align:middle;font-size:%s;', $font_size ) ) );
356+
357+
// Generate the label content
358+
$label_content = sprintf(
359+
'<a href="%1$s" rel="noopener nofollow" target="_blank" style="text-decoration:none; padding: 0.25em; padding-right: 17px; display: inline-block;"><span style="margin-left:.5em;margin-right:.5em;font-weight:bold"> %2$s </span></a>',
360+
esc_url( $audio_url ),
361+
esc_html( $label )
362+
);
363+
$label_cell_style = sprintf(
364+
'vertical-align:middle;font-size:%s;',
365+
$font_size
366+
);
367+
$label_content = \Automattic\WooCommerce\EmailEditor\Integrations\Utils\Table_Wrapper_Helper::render_table_cell( $label_content, array( 'style' => $label_cell_style ) );
368+
369+
// Combine icon and label tables
370+
$podcast_content = $icon_content . $label_content;
371+
372+
// Create the main pill-style table
373+
$main_table_styles = sprintf(
374+
'background-color: %s; border-radius: 9999px; display: inline-table; float: none; border: 1px solid %s; border-collapse: separate;',
375+
$background_color,
376+
$border_color
377+
);
378+
379+
$main_table_attrs = array(
380+
'align' => 'left',
381+
'style' => $main_table_styles,
382+
);
383+
384+
$main_table = \Automattic\WooCommerce\EmailEditor\Integrations\Utils\Table_Wrapper_Helper::render_table_wrapper( $podcast_content, $main_table_attrs, array(), array(), false );
385+
386+
// Create the main wrapper table
387+
$table_style = 'width: 100%;';
388+
if ( ! empty( $table_margin_style ) ) {
389+
$table_style = $table_margin_style . '; ' . $table_style;
390+
} else {
391+
$table_style = 'margin: 16px 0; ' . $table_style;
392+
}
393+
394+
$table_attrs = array(
395+
'style' => $table_style,
396+
);
397+
398+
$cell_attrs = array(
399+
'style' => 'min-width: 100%; vertical-align: middle; word-break: break-word; text-align: left;',
400+
);
401+
402+
$main_wrapper = \Automattic\WooCommerce\EmailEditor\Integrations\Utils\Table_Wrapper_Helper::render_table_wrapper( $main_table, $table_attrs, $cell_attrs );
403+
404+
return \Automattic\WooCommerce\EmailEditor\Integrations\Utils\Table_Wrapper_Helper::render_outlook_table_wrapper( $main_wrapper, array( 'align' => 'left' ) );
405+
}
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
<?php
2+
/**
3+
* Podcast Player Block Email Rendering tests
4+
*
5+
* @package automattic/jetpack
6+
*/
7+
8+
require_once JETPACK__PLUGIN_DIR . 'extensions/blocks/podcast-player/podcast-player.php';
9+
10+
// Include mock classes for WooCommerce Email Editor helpers
11+
require_once __DIR__ . '/class-mock-styles-helper.php';
12+
require_once __DIR__ . '/class-mock-table-wrapper-helper.php';
13+
14+
use PHPUnit\Framework\Attributes\CoversFunction;
15+
16+
/**
17+
* Podcast Player Block Email Rendering tests.
18+
*
19+
* These tests verify the render_email function works correctly for various scenarios
20+
* including valid inputs, security validation, and email rendering.
21+
*
22+
* @covers ::Automattic\Jetpack\Extensions\Podcast_Player\render_email
23+
*/
24+
#[CoversFunction( 'Automattic\Jetpack\Extensions\Podcast_Player\render_email' )]
25+
class Podcast_Player_Block_Email_Test extends WP_UnitTestCase {
26+
use \Automattic\Jetpack\PHPUnit\WP_UnitTestCase_Fix;
27+
28+
/**
29+
* Helper to create a parsed block with test podcast URL.
30+
*
31+
* @param array $attrs Optional custom attributes.
32+
* @return array Parsed block structure.
33+
*/
34+
private function create_parsed_block_with_attrs( $attrs = array() ) {
35+
$default_attrs = array(
36+
'url' => 'https://feeds.acast.com/public/shows/test-podcast',
37+
);
38+
39+
return array(
40+
'attrs' => array_merge( $default_attrs, $attrs ),
41+
);
42+
}
43+
44+
/**
45+
* Helper to create a rendering context mock.
46+
*
47+
* @param string $width The width to return from get_layout_width_without_padding.
48+
* @return object Mock rendering context.
49+
*/
50+
private function create_rendering_context_mock( $width = '600px' ) {
51+
return new class( $width ) {
52+
private $width;
53+
54+
public function __construct( $width ) {
55+
$this->width = $width;
56+
}
57+
58+
public function get_layout_width_without_padding() {
59+
return $this->width;
60+
}
61+
};
62+
}
63+
64+
/**
65+
* Test render_email with valid podcast URL.
66+
*/
67+
public function test_render_email_with_valid_podcast_url() {
68+
$parsed_block = $this->create_parsed_block_with_attrs();
69+
$mock_context = $this->create_rendering_context_mock();
70+
$result = \Automattic\Jetpack\Extensions\Podcast_Player\render_email( '', $parsed_block, $mock_context );
71+
72+
// Should return HTML content
73+
$this->assertNotEmpty( $result );
74+
$this->assertStringContainsString( '<table', $result );
75+
$this->assertStringContainsString( 'href=', $result );
76+
$this->assertStringContainsString( 'Listen to the podcast', $result );
77+
78+
// Should contain table-based layout for email compatibility
79+
$this->assertStringContainsString( 'border-collapse: collapse', $result );
80+
81+
// Should contain margin styling for email spacing
82+
$this->assertStringContainsString( 'margin: 16px 0', $result );
83+
}
84+
85+
/**
86+
* Test render_email with missing attrs.
87+
*/
88+
public function test_render_email_with_missing_attrs() {
89+
$mock_context = $this->create_rendering_context_mock();
90+
91+
// Test with missing attrs
92+
$result = \Automattic\Jetpack\Extensions\Podcast_Player\render_email( '', array( 'not-attrs' => array() ), $mock_context );
93+
$this->assertSame( '', $result );
94+
95+
// Test with empty attrs
96+
$result = \Automattic\Jetpack\Extensions\Podcast_Player\render_email( '', array( 'attrs' => array() ), $mock_context );
97+
$this->assertSame( '', $result );
98+
}
99+
100+
/**
101+
* Test render_email with empty URL.
102+
*/
103+
public function test_render_email_with_empty_url() {
104+
$parsed_block = $this->create_parsed_block_with_attrs(
105+
array(
106+
'url' => '',
107+
)
108+
);
109+
$mock_context = $this->create_rendering_context_mock();
110+
$result = \Automattic\Jetpack\Extensions\Podcast_Player\render_email( '', $parsed_block, $mock_context );
111+
112+
// Should return empty string when no valid URL
113+
$this->assertSame( '', $result );
114+
}
115+
116+
/**
117+
* Test render_email with invalid URL.
118+
*/
119+
public function test_render_email_with_invalid_url() {
120+
$parsed_block = $this->create_parsed_block_with_attrs(
121+
array(
122+
'url' => 'not-a-valid-url',
123+
)
124+
);
125+
$mock_context = $this->create_rendering_context_mock();
126+
$result = \Automattic\Jetpack\Extensions\Podcast_Player\render_email( '', $parsed_block, $mock_context );
127+
128+
// Should return empty string when URL is invalid
129+
$this->assertSame( '', $result );
130+
}
131+
132+
/**
133+
* Test render_email returns empty when WooCommerce Email Editor helper classes are missing.
134+
*/
135+
public function test_render_email_returns_empty_when_helpers_missing() {
136+
$parsed_block = $this->create_parsed_block_with_attrs();
137+
$mock_context = $this->create_rendering_context_mock();
138+
139+
// Verify that with mocked classes, the function works
140+
$result_with_mocks = \Automattic\Jetpack\Extensions\Podcast_Player\render_email( '', $parsed_block, $mock_context );
141+
$this->assertNotEmpty( $result_with_mocks );
142+
143+
// Test the class existence check logic directly
144+
$table_helper_exists = class_exists( '\Automattic\WooCommerce\EmailEditor\Integrations\Utils\Table_Wrapper_Helper' );
145+
146+
// Class should exist due to our mock
147+
$this->assertTrue( $table_helper_exists, 'Table_Wrapper_Helper class should be mocked and available' );
148+
}
149+
150+
/**
151+
* Test render_email security - URL validation.
152+
*/
153+
public function test_render_email_security_url_validation() {
154+
$parsed_block = $this->create_parsed_block_with_attrs(
155+
array(
156+
'url' => 'https://feeds.acast.com/public/shows/test-podcast',
157+
)
158+
);
159+
$mock_context = $this->create_rendering_context_mock();
160+
$result = \Automattic\Jetpack\Extensions\Podcast_Player\render_email( '', $parsed_block, $mock_context );
161+
162+
// Should render with valid URL
163+
$this->assertNotEmpty( $result );
164+
$this->assertStringContainsString( 'href="https://feeds.acast.com/public/shows/test-podcast"', $result );
165+
}
166+
167+
/**
168+
* Test render_email contains proper button styling.
169+
*/
170+
public function test_render_email_contains_button_styling() {
171+
$parsed_block = $this->create_parsed_block_with_attrs();
172+
$mock_context = $this->create_rendering_context_mock();
173+
$result = \Automattic\Jetpack\Extensions\Podcast_Player\render_email( '', $parsed_block, $mock_context );
174+
175+
// Should contain button styling
176+
$this->assertNotEmpty( $result );
177+
$this->assertStringContainsString( 'background-color: #f6f7f7', $result );
178+
$this->assertStringContainsString( 'border: 1px solid #AAA', $result );
179+
$this->assertStringContainsString( 'border-radius: 9999px', $result );
180+
}
181+
182+
/**
183+
* Test render_email contains play icon.
184+
*/
185+
public function test_render_email_contains_play_icon() {
186+
$parsed_block = $this->create_parsed_block_with_attrs();
187+
$mock_context = $this->create_rendering_context_mock();
188+
$result = \Automattic\Jetpack\Extensions\Podcast_Player\render_email( '', $parsed_block, $mock_context );
189+
190+
// Should contain play icon
191+
$this->assertNotEmpty( $result );
192+
$this->assertStringContainsString( 'audio-play.png', $result );
193+
$this->assertStringContainsString( '<img', $result );
194+
}
195+
}

projects/plugins/jetpack/tests/php/extensions/blocks/class-mock-table-wrapper-helper.php

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,30 @@ class Mock_Table_Wrapper_Helper {
1414
/**
1515
* Mock render_table_wrapper method
1616
*
17-
* @param string $content The content to wrap.
18-
* @param array $attributes The table attributes.
17+
* @param string $content The content to wrap.
18+
* @param array $table_attrs Table attributes.
19+
* @param array $cell_attrs Cell attributes.
20+
* @param array $row_attrs Row attributes.
21+
* @param bool $render_cell Whether to render the td wrapper.
1922
* @return string The wrapped table HTML.
2023
*/
21-
public static function render_table_wrapper( $content, $attributes ) {
24+
public static function render_table_wrapper( $content, $table_attrs = array(), $cell_attrs = array(), $row_attrs = array(), $render_cell = true ) {
2225
// Simple mock that wraps content in a table
23-
$style = $attributes['style'] ?? '';
24-
$width = $attributes['width'] ?? 600;
26+
$style = $table_attrs['style'] ?? '';
27+
$width = $table_attrs['width'] ?? 600;
2528

26-
return sprintf(
29+
$table_html = sprintf(
2730
'<table role="presentation" style="width: 100%%; max-width: %dpx; margin: 16px auto; border-collapse: collapse; padding: 0; %s">',
2831
$width,
2932
$style
30-
) . '<tr><td style="padding: 0; font-family: Arial, sans-serif;">' . $content . '</td></tr></table>';
33+
);
34+
35+
if ( $render_cell ) {
36+
$cell_style = $cell_attrs['style'] ?? '';
37+
$content = '<td style="padding: 0; font-family: Arial, sans-serif; ' . $cell_style . '">' . $content . '</td>';
38+
}
39+
40+
return $table_html . '<tr>' . $content . '</tr></table>';
3141
}
3242

3343
/**
@@ -47,6 +57,25 @@ public static function render_table_cell( $content, $attributes ) {
4757
$content
4858
);
4959
}
60+
61+
/**
62+
* Mock render_outlook_table_wrapper method
63+
*
64+
* @param string $content The content to wrap.
65+
* @param array $attributes The table attributes.
66+
* @return string The wrapped table HTML.
67+
*/
68+
public static function render_outlook_table_wrapper( $content, $attributes ) {
69+
// Simple mock that wraps content in a table for Outlook compatibility
70+
$style = $attributes['style'] ?? '';
71+
$align = $attributes['align'] ?? 'left';
72+
73+
return sprintf(
74+
'<table role="presentation" style="width: 100%%; max-width: 600px; margin: 16px auto; border-collapse: collapse; padding: 0; text-align: %s; %s">',
75+
$align,
76+
$style
77+
) . '<tr><td style="padding: 0; font-family: Arial, sans-serif;">' . $content . '</td></tr></table>';
78+
}
5079
}
5180
class_alias( 'Mock_Table_Wrapper_Helper', '\Automattic\WooCommerce\EmailEditor\Integrations\Utils\Table_Wrapper_Helper' );
5281
}

0 commit comments

Comments
 (0)