Improve SportsPress Open Graph images

This commit is contained in:
2026-04-27 12:43:26 -05:00
parent 1df307dfbe
commit 2afe98bc99
6 changed files with 1868 additions and 344 deletions

View File

@@ -0,0 +1,222 @@
<?php
/**
* Tests for the SportsPress event image generator.
*
* @package Tonys_Sportspress_Enhancements
*/
/**
* Featured image generator tests.
*/
class Test_Featured_Image_Generator extends WP_UnitTestCase {
/**
* Temp files created during a test.
*
* @var string[]
*/
private $temp_files = array();
/**
* Clean up temp files.
*/
public function tear_down(): void {
foreach ( $this->temp_files as $file ) {
if ( file_exists( $file ) ) {
unlink( $file );
}
}
$this->temp_files = array();
parent::tear_down();
}
/**
* Create a post.
*
* @param string $type Post type.
* @param string $title Title.
* @return int
*/
private function create_post_of_type( $type, $title ) {
return self::factory()->post->create(
array(
'post_type' => $type,
'post_title' => $title,
'post_status' => 'publish',
)
);
}
/**
* Create a small raster fixture.
*
* @param string $extension File extension.
* @return string
*/
private function create_raster_fixture( $extension ) {
$image = imagecreatetruecolor( 24, 24 );
$red = imagecolorallocate( $image, 200, 0, 0 );
imagefilledrectangle( $image, 0, 0, 23, 23, $red );
$file = tempnam( sys_get_temp_dir(), 'sp-img-' );
$path = $file . '.' . $extension;
rename( $file, $path );
switch ( $extension ) {
case 'jpg':
imagejpeg( $image, $path );
break;
case 'gif':
imagegif( $image, $path );
break;
case 'webp':
imagewebp( $image, $path );
break;
case 'png':
default:
imagepng( $image, $path );
break;
}
asc_sp_event_image_destroy( $image );
$this->temp_files[] = $path;
return $path;
}
/**
* Invalid IDs and non-event posts produce request errors.
*/
public function test_invalid_and_non_event_requests_prepare_404_errors() {
$this->assertWPError( asc_sp_event_prepare_image_request( 999999 ) );
$post_id = $this->create_post_of_type( 'post', 'Regular Post' );
$error = asc_sp_event_prepare_image_request( $post_id );
$this->assertWPError( $error );
$this->assertSame( 'invalid_event', $error->get_error_code() );
}
/**
* Missing team logo paths fall back to generated text and valid dimensions.
*/
public function test_missing_logo_path_generates_png_with_expected_dimensions() {
$image_data = generate_bisected_image( '#123456', '#abcdef', '/missing-left.png', '/missing-right.png', 'Hawks', 'Electrons' );
$image = imagecreatefromstring( $image_data );
$this->assertNotFalse( $image );
$this->assertSame( 1200, imagesx( $image ) );
$this->assertSame( 628, imagesy( $image ) );
asc_sp_event_image_destroy( $image );
}
/**
* Square image variant generates square PNG dimensions.
*/
public function test_square_variant_generates_expected_dimensions() {
$dimensions = asc_sp_event_image_variant_dimensions( 'square' );
$image_data = generate_bisected_image( '#123456', '#abcdef', '/missing-left.png', '/missing-right.png', 'Hawks', 'Electrons', $dimensions['width'], $dimensions['height'] );
$image = imagecreatefromstring( $image_data );
$this->assertNotFalse( $image );
$this->assertSame( 1200, imagesx( $image ) );
$this->assertSame( 1200, imagesy( $image ) );
asc_sp_event_image_destroy( $image );
}
/**
* Raster loader supports common GD-backed formats.
*/
public function test_raster_loader_supports_common_formats_when_available() {
$formats = array(
'png' => 'imagecreatefrompng',
'jpg' => 'imagecreatefromjpeg',
'gif' => 'imagecreatefromgif',
);
if ( function_exists( 'imagewebp' ) && function_exists( 'imagecreatefromwebp' ) ) {
$formats['webp'] = 'imagecreatefromwebp';
}
foreach ( $formats as $extension => $function ) {
if ( ! function_exists( $function ) ) {
continue;
}
$path = $this->create_raster_fixture( $extension );
$image = asc_sp_event_image_create_from_file( $path );
$this->assertNotFalse( $image, "Failed loading {$extension}" );
$this->assertSame( 24, imagesx( $image ) );
$this->assertSame( 24, imagesy( $image ) );
asc_sp_event_image_destroy( $image );
}
}
/**
* Bundled sporty font is available for fallback text.
*/
public function test_bundled_bebas_neue_font_is_available() {
$this->assertFileExists( asc_sp_event_image_font_path() );
$this->assertIsReadable( asc_sp_event_image_font_path() );
}
/**
* Prepared event request includes fallback text for missing logos.
*/
public function test_prepare_image_request_uses_team_short_name_fallbacks() {
$team1 = $this->create_post_of_type( 'sp_team', 'Hawks' );
$team2 = $this->create_post_of_type( 'sp_team', 'Electrons' );
$event = $this->create_post_of_type( 'sp_event', 'Hawks vs Electrons' );
add_post_meta( $event, 'sp_team', $team1 );
add_post_meta( $event, 'sp_team', $team2 );
$request = asc_sp_event_prepare_image_request( $event );
$this->assertIsArray( $request );
$this->assertSame( 'Hawks', $request['team1_fallback'] );
$this->assertSame( 'Electrons', $request['team2_fallback'] );
$this->assertSame( '', $request['team1_logo'] );
$this->assertSame( '', $request['team2_logo'] );
}
/**
* Invalid colors are safely normalized.
*/
public function test_invalid_colors_fall_back_to_configured_defaults() {
$this->assertSame( '#4B5563', asc_sp_event_image_color( 'not-a-color' ) );
$this->assertSame( '#6B7280', asc_sp_event_image_color( 'not-a-color', '#6B7280' ) );
$this->assertSame( '#112233', asc_sp_event_image_color( '#112233' ) );
}
/**
* Image cache keys include the generator version and style hash.
*/
public function test_prepare_image_request_uses_versioned_style_cache_key() {
$team1 = $this->create_post_of_type( 'sp_team', 'Hawks' );
$team2 = $this->create_post_of_type( 'sp_team', 'Electrons' );
$event = $this->create_post_of_type( 'sp_event', 'Hawks vs Electrons' );
add_post_meta( $event, 'sp_team', $team1 );
add_post_meta( $event, 'sp_team', $team2 );
$request = asc_sp_event_prepare_image_request( $event );
$this->assertStringStartsWith( 'team_image_v' . ASC_SP_EVENT_IMAGE_CACHE_VERSION . '_' . asc_sp_event_image_cache_style_hash(), $request['cache_key'] );
$this->assertSame( 'wide', $request['variant'] );
$this->assertSame( 1200, $request['width'] );
$this->assertSame( 628, $request['height'] );
$square_request = asc_sp_event_prepare_image_request( $event, 'square' );
$this->assertStringContainsString( '_square_', $square_request['cache_key'] );
$this->assertSame( 'square', $square_request['variant'] );
$this->assertSame( 1200, $square_request['width'] );
$this->assertSame( 1200, $square_request['height'] );
}
}

View File

@@ -0,0 +1,271 @@
<?php
/**
* Tests for SportsPress event Open Graph output.
*
* @package Tonys_Sportspress_Enhancements
*/
if ( ! class_exists( 'SP_Event' ) ) {
/**
* Minimal SportsPress event test double.
*/
class SP_Event {
/**
* Event post ID.
*
* @var int
*/
private $id;
/**
* Status values by event ID.
*
* @var array<int,string>
*/
public static $statuses = array();
/**
* Result values by event ID.
*
* @var array<int,array>
*/
public static $results = array();
/**
* Constructor.
*
* @param int $id Event post ID.
*/
public function __construct( $id ) {
$this->id = absint( $id );
}
/**
* Get event status.
*
* @return string
*/
public function status() {
return self::$statuses[ $this->id ] ?? '';
}
/**
* Get event results.
*
* @return array
*/
public function results() {
return self::$results[ $this->id ] ?? array();
}
}
}
/**
* Open Graph tests.
*/
class Test_Open_Graph_Tags extends WP_UnitTestCase {
/**
* Reset mock SportsPress state.
*/
public function set_up(): void {
parent::set_up();
if ( property_exists( 'SP_Event', 'statuses' ) ) {
SP_Event::$statuses = array();
SP_Event::$results = array();
}
update_option( 'sportspress_event_reverse_teams', 'no' );
update_option( 'sportspress_event_teams_delimiter', 'vs' );
}
/**
* Create a team.
*
* @param string $name Team name.
* @return int
*/
private function create_team( $name ) {
return self::factory()->post->create(
array(
'post_type' => 'sp_team',
'post_title' => $name,
)
);
}
/**
* Create an event.
*
* @param array $args Post args.
* @return int
*/
private function create_event( array $args = array() ) {
return self::factory()->post->create(
wp_parse_args(
$args,
array(
'post_type' => 'sp_event',
'post_title' => 'Test Event',
'post_status' => 'future',
'post_date' => '2026-05-02 13:00:00',
'post_content' => 'First pitch at one.',
)
)
);
}
/**
* Future event emits complete Open Graph data.
*/
public function test_future_event_emits_core_open_graph_values() {
$home = $this->create_team( 'Hawks' );
$away = $this->create_team( 'Electrons' );
$event = $this->create_event();
add_post_meta( $event, 'sp_team', $home );
add_post_meta( $event, 'sp_team', $away );
if ( property_exists( 'SP_Event', 'statuses' ) ) {
SP_Event::$statuses[ $event ] = 'future';
}
$meta = asc_sp_event_open_graph_data( $event );
$this->assertSame( 'article', $meta['type'] );
$this->assertStringContainsString( 'Hawks vs Electrons', $meta['title'] );
$this->assertStringContainsString( 'First pitch at one.', $meta['description'] );
$this->assertCount( 2, $meta['images'] );
$this->assertSame( '1200', $meta['images'][0]['width'] );
$this->assertSame( '628', $meta['images'][0]['height'] );
$this->assertSame( '1200', $meta['images'][1]['width'] );
$this->assertSame( '1200', $meta['images'][1]['height'] );
$this->assertSame( '1200', $meta['image_width'] );
$this->assertSame( '628', $meta['image_height'] );
$this->assertStringContainsString( '/head-to-head?post=' . $event, $meta['image'] );
$this->assertStringContainsString( 'variant=square', $meta['images'][1]['url'] );
$this->assertNotEmpty( $meta['url'] );
}
/**
* Postponed, cancelled, and TBD labels appear in title and description.
*
* @dataProvider status_provider
*
* @param string $status Status slug.
*/
public function test_schedule_status_appears_in_title_and_description( $status ) {
$home = $this->create_team( 'Hawks' );
$away = $this->create_team( 'Electrons' );
$event = $this->create_event();
add_post_meta( $event, 'sp_team', $home );
add_post_meta( $event, 'sp_team', $away );
update_post_meta( $event, 'sp_status', $status );
$meta = asc_sp_event_open_graph_data( $event );
$label = strtoupper( $status );
$this->assertStringStartsWith( $label, $meta['title'] );
$this->assertStringStartsWith( $label, $meta['description'] );
}
/**
* Status provider.
*
* @return array
*/
public function status_provider() {
return array(
array( 'postponed' ),
array( 'cancelled' ),
array( 'tbd' ),
);
}
/**
* Result events with scores emit score titles.
*/
public function test_result_event_with_scores_emits_score_title() {
$home = $this->create_team( 'Hawks' );
$away = $this->create_team( 'Electrons' );
$event = $this->create_event( array( 'post_status' => 'publish' ) );
add_post_meta( $event, 'sp_team', $home );
add_post_meta( $event, 'sp_team', $away );
if ( property_exists( 'SP_Event', 'statuses' ) ) {
SP_Event::$statuses[ $event ] = 'results';
SP_Event::$results[ $event ] = array(
0 => array( 'r' => 'R' ),
$home => array( 'r' => '7' ),
$away => array( 'r' => '4' ),
);
}
$meta = asc_sp_event_open_graph_data( $event );
$this->assertStringContainsString( 'Hawks 7-4 Electrons', $meta['title'] );
}
/**
* Missing teams/results/outcomes still produce valid data.
*/
public function test_missing_sportspress_data_does_not_break_meta_generation() {
$event = $this->create_event(
array(
'post_title' => 'Sparse Event',
'post_content' => '',
)
);
if ( property_exists( 'SP_Event', 'statuses' ) ) {
SP_Event::$statuses[ $event ] = 'results';
SP_Event::$results[ $event ] = array();
}
$meta = asc_sp_event_open_graph_data( $event );
$this->assertSame( 'Sparse Event', $meta['title'] );
$this->assertNotEmpty( $meta['description'] );
$this->assertSame( '1200', $meta['image_width'] );
}
/**
* HTML-heavy post content is stripped and escaped in rendered tags.
*/
public function test_description_strips_html_and_rendered_tags_are_escaped() {
$home = $this->create_team( 'Hawks "A"' );
$away = $this->create_team( 'Electrons <B>' );
$event = $this->create_event(
array(
'post_content' => '<script>alert("x")</script><p>Bring <strong>bats</strong> & gloves.</p>',
)
);
add_post_meta( $event, 'sp_team', $home );
add_post_meta( $event, 'sp_team', $away );
$meta = asc_sp_event_open_graph_data( $event );
$this->assertStringNotContainsString( '<script', $meta['description'] );
$this->assertStringContainsString( 'Bring bats & gloves.', $meta['description'] );
$GLOBALS['post'] = get_post( $event );
$GLOBALS['wp_query']->is_single = true;
ob_start();
custom_open_graph_tags_with_sportspress_integration();
$output = ob_get_clean();
$this->assertStringContainsString( 'og:image:width', $output );
$this->assertSame( 2, substr_count( $output, 'property="og:image" content=' ) );
$this->assertStringContainsString( 'content="628"', $output );
$this->assertStringContainsString( 'content="1200"', $output );
$this->assertStringContainsString( 'variant=square', $output );
$this->assertStringContainsString( 'Hawks &quot;A&quot;', $output );
$this->assertStringNotContainsString( '<B>', $output );
$this->assertStringNotContainsString( '<script', $output );
}
}