diff --git a/admin/section/class-convertkit-admin-section-broadcasts.php b/admin/section/class-convertkit-admin-section-broadcasts.php index 6afa9f133..d9587e5fa 100644 --- a/admin/section/class-convertkit-admin-section-broadcasts.php +++ b/admin/section/class-convertkit-admin-section-broadcasts.php @@ -349,7 +349,46 @@ public function register_fields() { array( 'name' => 'export_public', 'label_for' => 'export_public', - 'label' => __( 'If enabled, draft broadcasts will be published to your Kit Creator Profile and Landing Pages when sent.', 'convertkit' ), + 'label' => __( 'If enabled, draft broadcasts will be published to your Kit Newsletter site and Landing Pages when sent.', 'convertkit' ), + ) + ); + + add_settings_field( + 'export_greeting', + __( 'Greeting', 'convertkit' ), + array( $this, 'export_greeting_callback' ), + $this->settings_key, + $this->name . '-export', + array( + 'name' => 'export_greeting', + 'label_for' => 'export_greeting', + 'description' => __( 'Optional. Added to the start of draft broadcasts. Supports HTML and Liquid, for example: Hi {{ subscriber.first_name | default: "there" }},', 'convertkit' ), + ) + ); + + add_settings_field( + 'export_sign_off', + __( 'Sign-off', 'convertkit' ), + array( $this, 'export_sign_off_callback' ), + $this->settings_key, + $this->name . '-export', + array( + 'name' => 'export_sign_off', + 'label_for' => 'export_sign_off', + 'description' => __( 'Optional. Added to the end of draft broadcasts. Supports HTML and Liquid.', 'convertkit' ), + ) + ); + + add_settings_field( + 'export_web_link', + __( 'Read on the Web', 'convertkit' ), + array( $this, 'export_web_link_callback' ), + $this->settings_key, + $this->name . '-export', + array( + 'name' => 'export_web_link', + 'label_for' => $this->settings_key . '_export_web_link', + 'description' => __( 'Adds a link to read the post on your site to draft broadcasts. Only added once the post is published.', 'convertkit' ), ) ); @@ -720,6 +759,76 @@ public function export_public_callback( $args ) { } + /** + * Renders the input for the Greeting setting. + * + * @since 3.4.8 + * + * @param array $args Setting field arguments (name,description). + */ + public function export_greeting_callback( $args ) { + + // Output field. + $this->output_textarea_field( + $args['name'], + esc_textarea( $this->settings->export_greeting() ), + $args['description'], + array( + 'large-text', + 'enabled_export', + ) + ); + + } + + /** + * Renders the input for the Sign-off setting. + * + * @since 3.4.8 + * + * @param array $args Setting field arguments (name,description). + */ + public function export_sign_off_callback( $args ) { + + // Output field. + $this->output_textarea_field( + $args['name'], + esc_textarea( $this->settings->export_sign_off() ), + $args['description'], + array( + 'large-text', + 'enabled_export', + ) + ); + + } + + /** + * Renders the input for the Read on the Web setting. + * + * @since 3.4.8 + * + * @param array $args Setting field arguments (name,description). + */ + public function export_web_link_callback( $args ) { + + // Output field. + $this->output_select_field( + $args['name'], + $this->settings->export_web_link(), + array( + '' => esc_html__( 'None', 'convertkit' ), + 'top' => esc_html__( 'Top of broadcast', 'convertkit' ), + 'bottom' => esc_html__( 'Bottom of broadcast', 'convertkit' ), + ), + $args['description'], + array( + 'enabled_export', + ) + ); + + } + /** * Renders the input for the No Styles setting. * @@ -763,6 +872,13 @@ public function sanitize_settings( $settings ) { $settings['import_images'] = ''; } + // Remove disallowed HTML from the greeting and sign-off. + foreach ( array( 'export_greeting', 'export_sign_off' ) as $key ) { + if ( array_key_exists( $key, $settings ) ) { + $settings[ $key ] = wp_kses_post( $settings[ $key ] ); + } + } + // Merge settings with defaults. $settings = wp_parse_args( $settings, $this->settings->get_defaults() ); diff --git a/includes/class-convertkit-broadcasts-exporter.php b/includes/class-convertkit-broadcasts-exporter.php index 6eddf7385..87f809966 100644 --- a/includes/class-convertkit-broadcasts-exporter.php +++ b/includes/class-convertkit-broadcasts-exporter.php @@ -248,6 +248,9 @@ public function export_post_to_broadcast( $post_id ) { // Remove responsive image attributes, which email clients don't support. $content = $this->remove_responsive_image_attributes( $content ); + // Add the greeting, sign-off and link to read the Post on the web. + $content = $this->add_greeting_sign_off_and_web_link( $content, $post ); + // Convert relative URLs to absolute URLs, as emails have no site context. $content = links_add_base_url( $content, get_permalink( $post ) ); @@ -270,7 +273,8 @@ public function export_post_to_broadcast( $post_id ) { } // Define the Broadcast's parameters. - $args = array( + $thumbnail = $this->get_thumbnail( $post ); + $args = array( $post->post_title, $content, $post->post_excerpt, @@ -279,9 +283,9 @@ public function export_post_to_broadcast( $post_id ) { null, '', '', - '', - '', - '', + $thumbnail['alt'], + $thumbnail['url'], + $this->get_preview_text( $post ), $this->get_subscriber_filter(), ); @@ -306,6 +310,32 @@ public function export_post_to_broadcast( $post_id ) { } + /** + * Returns the preview text for the Broadcast, using the Post's excerpt if one was written. + * + * @since 3.4.8 + * + * @param WP_Post $post WordPress Post. + * @return string Preview text + */ + private function get_preview_text( $post ) { + + $preview_text = ( has_excerpt( $post ) ? $post->post_excerpt : '' ); + + /** + * Define the preview text for the Broadcast created from the WordPress Post. + * + * @since 3.4.8 + * + * @param string $preview_text Preview text. + * @param WP_Post $post WordPress Post. + */ + $preview_text = apply_filters( 'convertkit_broadcasts_exporter_preview_text', $preview_text, $post ); + + return (string) $preview_text; + + } + /** * Returns the Broadcast ID the given Post was previously exported to, if any. * @@ -364,6 +394,46 @@ private function get_existing_draft_broadcast_id( $api, $post_id ) { } + /** + * Returns the Post's featured image URL and alt text, to use as the Broadcast's thumbnail + * if the Broadcast is public. + * + * @since 3.4.8 + * + * @param WP_Post $post WordPress Post. + * @return array Thumbnail URL and alt text (blank if no thumbnail) + */ + private function get_thumbnail( $post ) { + + $thumbnail = array( + 'url' => '', + 'alt' => '', + ); + + // Bail if the Broadcast isn't public, as Kit only uses thumbnails for public Broadcasts. + if ( ! $this->broadcasts_settings->export_public() ) { + return $thumbnail; + } + + // Bail if the Post has no featured image. + $image_id = get_post_thumbnail_id( $post ); + if ( ! $image_id ) { + return $thumbnail; + } + + // Bail if the featured image's URL can't be fetched. + $url = wp_get_attachment_image_url( $image_id, 'full' ); + if ( ! $url ) { + return $thumbnail; + } + + return array( + 'url' => $url, + 'alt' => (string) get_post_meta( $image_id, '_wp_attachment_image_alt', true ), + ); + + } + /** * Returns the Post's published date and time, if the Broadcast is public and the Post is published. * @@ -430,6 +500,57 @@ private function get_subscriber_filter() { } + /** + * Adds the greeting, sign-off and link to read the Post on the web to the given content, + * based on the Plugin's settings. + * + * @since 3.4.8 + * + * @param string $content HTML Content. + * @param WP_Post $post WordPress Post. + * @return string HTML Content + */ + private function add_greeting_sign_off_and_web_link( $content, $post ) { + + // Add the greeting and sign-off, converting any line breaks to paragraphs. + if ( $this->broadcasts_settings->export_greeting() ) { + $content = wpautop( $this->broadcasts_settings->export_greeting() ) . $content; + } + if ( $this->broadcasts_settings->export_sign_off() ) { + $content .= wpautop( $this->broadcasts_settings->export_sign_off() ); + } + + // Bail if no link to read the Post on the web is required, or the Post isn't published, as its link won't work. + if ( ! $this->broadcasts_settings->export_web_link() || $post->post_status !== 'publish' ) { + return $content; + } + + /** + * Define the text for the link to read the Post on the web, in Broadcasts created from WordPress Posts. + * + * @since 3.4.8 + * + * @param string $text Link text. + * @param WP_Post $post WordPress Post. + */ + $text = apply_filters( 'convertkit_broadcasts_exporter_web_link_text', __( 'Read this on the web', 'convertkit' ), $post ); + + // Build link. + $link = sprintf( + '

%s

', + esc_url( get_permalink( $post ) ), + esc_html( $text ) + ); + + // Add link to the top or bottom of the content. + if ( $this->broadcasts_settings->export_web_link() === 'top' ) { + return $link . $content; + } + + return $content . $link; + + } + /** * Removes `srcset` and `sizes` attributes from images in the given content. * diff --git a/includes/class-convertkit-settings-broadcasts.php b/includes/class-convertkit-settings-broadcasts.php index 27339bb06..adc6efdd5 100644 --- a/includes/class-convertkit-settings-broadcasts.php +++ b/includes/class-convertkit-settings-broadcasts.php @@ -203,6 +203,46 @@ public function export_public() { } + /** + * Returns the greeting to add to the start of Broadcasts created from WordPress Posts. + * + * @since 3.4.8 + * + * @return string HTML, which may include Liquid + */ + public function export_greeting() { + + return (string) $this->settings['export_greeting']; + + } + + /** + * Returns the sign-off to add to the end of Broadcasts created from WordPress Posts. + * + * @since 3.4.8 + * + * @return string HTML, which may include Liquid + */ + public function export_sign_off() { + + return (string) $this->settings['export_sign_off']; + + } + + /** + * Returns where to add a link to read the WordPress Post on the web, in Broadcasts + * created from WordPress Posts. + * + * @since 3.4.8 + * + * @return string Position (blank = none, top, bottom) + */ + public function export_web_link() { + + return ( in_array( $this->settings['export_web_link'], array( 'top', 'bottom' ), true ) ? $this->settings['export_web_link'] : '' ); + + } + /** * Returns whether Broadcasts should have their styles imported. * @@ -316,7 +356,20 @@ public function get_schema() { 'export_public' => array( 'type' => 'string', 'enum' => array( '', 'on' ), - 'description' => __( 'Whether Broadcasts created from WordPress Posts are published to the web (Kit Creator Profile and Landing Pages).', 'convertkit' ), + 'description' => __( 'Whether Broadcasts created from WordPress Posts are published to the web (Kit Newsletter site and Landing Pages).', 'convertkit' ), + ), + 'export_greeting' => array( + 'type' => 'string', + 'description' => __( 'HTML to add to the start of Broadcasts created from WordPress Posts. Supports Liquid e.g. {{ subscriber.first_name }}.', 'convertkit' ), + ), + 'export_sign_off' => array( + 'type' => 'string', + 'description' => __( 'HTML to add to the end of Broadcasts created from WordPress Posts. Supports Liquid.', 'convertkit' ), + ), + 'export_web_link' => array( + 'type' => 'string', + 'enum' => array( '', 'top', 'bottom' ), + 'description' => __( 'Where to add a link to read the published WordPress Post on the web, in Broadcasts created from WordPress Posts. Blank for none.', 'convertkit' ), ), 'no_styles' => array( 'type' => 'string', @@ -352,6 +405,9 @@ public function get_defaults() { 'enabled_export' => '', 'export_send_to' => '', 'export_public' => '', + 'export_greeting' => '', + 'export_sign_off' => '', + 'export_web_link' => '', 'no_styles' => '', ); @@ -378,6 +434,13 @@ public function get_defaults() { */ public function save( $settings ) { + // Remove disallowed HTML from the greeting and sign-off. + foreach ( array( 'export_greeting', 'export_sign_off' ) as $key ) { + if ( array_key_exists( $key, $settings ) ) { + $settings[ $key ] = wp_kses_post( $settings[ $key ] ); + } + } + update_option( self::SETTINGS_NAME, array_merge( $this->get(), $settings ) ); // Reload settings in class, to reflect changes. diff --git a/resources/backend/js/settings-conditional-display.js b/resources/backend/js/settings-conditional-display.js index 26027b946..04ede10f2 100644 --- a/resources/backend/js/settings-conditional-display.js +++ b/resources/backend/js/settings-conditional-display.js @@ -46,10 +46,10 @@ function convertKitConditionallyDisplaySettings(input) { return; } - // Show or hide this row if the input, select, link or span element within the row has the CSS class of the setting ID. + // Show or hide this row if the input, select, textarea, link or span element within the row has the CSS class of the setting ID. if ( row.querySelector( - `input.${input.id}, select.${input.id}, a.${input.id}, span.${input.id}` + `input.${input.id}, select.${input.id}, textarea.${input.id}, a.${input.id}, span.${input.id}` ) ) { row.style.display = input.checked ? '' : 'none'; diff --git a/tests/EndToEnd/broadcasts/import-export/BroadcastsToPostsSettingsCest.php b/tests/EndToEnd/broadcasts/import-export/BroadcastsToPostsSettingsCest.php index 664354f50..cbbcdaaea 100644 --- a/tests/EndToEnd/broadcasts/import-export/BroadcastsToPostsSettingsCest.php +++ b/tests/EndToEnd/broadcasts/import-export/BroadcastsToPostsSettingsCest.php @@ -265,6 +265,39 @@ public function testSaveSendToSegment(EndToEndTester $I) $I->seeInField('_wp_convertkit_settings_broadcasts[export_send_to]', $_ENV['CONVERTKIT_API_SEGMENT_NAME']); } + /** + * Tests that saving the Greeting, Sign-off and Read on the Web settings works. + * + * @since 3.4.8 + * + * @param EndToEndTester $I Tester. + */ + public function testSaveGreetingSignOffAndWebLink(EndToEndTester $I) + { + // Go to the Plugin's Broadcasts Screen. + $I->loadKitSettingsBroadcastsScreen($I); + + // Enable export, and define the Greeting, Sign-off and Read on the Web settings. + $I->checkOption('#enabled_export'); + $I->fillField('_wp_convertkit_settings_broadcasts[export_greeting]', 'Hi {{ subscriber.first_name | default: "there" }},'); + $I->fillField('_wp_convertkit_settings_broadcasts[export_sign_off]', 'Thanks'); + $I->selectOption('_wp_convertkit_settings_broadcasts[export_web_link]', 'Bottom of broadcast'); + + // Click the Save Changes button. + $I->click('Save Changes'); + + // Wait for the settings to save. + $I->waitForElementVisible('#setting-error-settings_updated'); + + // Check that no PHP warnings or notices were output. + $I->checkNoWarningsAndNoticesOnScreen($I); + + // Confirm that the settings saved, keeping Liquid and permitted HTML, and removing scripts. + $I->seeInField('_wp_convertkit_settings_broadcasts[export_greeting]', 'Hi {{ subscriber.first_name | default: "there" }},'); + $I->seeInField('_wp_convertkit_settings_broadcasts[export_sign_off]', 'Thanksalert(1)'); + $I->seeOptionIsSelected('_wp_convertkit_settings_broadcasts[export_web_link]', 'Bottom of broadcast'); + } + /** * Deactivate and reset Plugin(s) after each test, if the test passes. * We don't use _after, as this would provide a screenshot of the Plugin diff --git a/tests/Integration/BroadcastsExportTest.php b/tests/Integration/BroadcastsExportTest.php index 2e6ddab95..8c83b1f0b 100644 --- a/tests/Integration/BroadcastsExportTest.php +++ b/tests/Integration/BroadcastsExportTest.php @@ -156,6 +156,88 @@ public function testExportRemovesResponsiveImageAttributes() $this->assertStringNotContainsString( 'sizes=', $this->request['content'] ); } + /** + * Test that the Post's excerpt is used as the Broadcast's preview text. + * + * @since 3.4.8 + */ + public function testExportPreviewTextUsesExcerpt() + { + // Create Post with an excerpt. + $post_id = $this->factory->post->create( + [ + 'post_title' => 'Kit: Export: Preview Text', + 'post_content' => '

Post content.

', + 'post_excerpt' => 'Post excerpt.', + ] + ); + + // Export Post. + $result = $this->exporter->export_post_to_broadcast( $post_id ); + $this->assertNotInstanceOf(\WP_Error::class, $result); + + // Confirm the excerpt is used as the preview text. + $this->assertEquals( 'Post excerpt.', $this->request['preview_text'] ); + } + + /** + * Test that no preview text is sent when the Post has no excerpt. + * + * @since 3.4.8 + */ + public function testExportPreviewTextWhenNoExcerpt() + { + // Create Post without an excerpt. + $post_id = $this->factory->post->create( + [ + 'post_title' => 'Kit: Export: Preview Text: No Excerpt', + 'post_content' => '

Post content.

', + 'post_excerpt' => '', + ] + ); + + // Export Post. + $result = $this->exporter->export_post_to_broadcast( $post_id ); + $this->assertNotInstanceOf(\WP_Error::class, $result); + + // Confirm no preview text is sent, so Kit doesn't use an auto-generated excerpt. + $this->assertArrayNotHasKey( 'preview_text', $this->request ); + } + + /** + * Test that the convertkit_broadcasts_exporter_preview_text filter defines the preview text. + * + * @since 3.4.8 + */ + public function testExportPreviewTextFilter() + { + // Define preview text using the filter. + add_filter( + 'convertkit_broadcasts_exporter_preview_text', + function ( $preview_text, $post ) { + return 'Filtered preview text for ' . $post->post_title; + }, + 10, + 2 + ); + + // Create Post with an excerpt. + $post_id = $this->factory->post->create( + [ + 'post_title' => 'Kit: Export: Preview Text: Filter', + 'post_content' => '

Post content.

', + 'post_excerpt' => 'Post excerpt.', + ] + ); + + // Export Post. + $result = $this->exporter->export_post_to_broadcast( $post_id ); + $this->assertNotInstanceOf(\WP_Error::class, $result); + + // Confirm the filtered preview text is used. + $this->assertEquals( 'Filtered preview text for Kit: Export: Preview Text: Filter', $this->request['preview_text'] ); + } + /** * Test that Broadcasts are sent to all subscribers and aren't public by default. * @@ -314,6 +396,206 @@ public function testExportPublicDraftPost() $this->assertArrayNotHasKey( 'published_at', $this->request ); } + /** + * Test that the Greeting and Sign-off settings are added to the start and end of the Broadcast, + * keeping Liquid tags. + * + * @since 3.4.8 + */ + public function testExportGreetingAndSignOff() + { + // Define Greeting and Sign-off settings. + $this->saveBroadcastsSettings( + [ + 'export_greeting' => 'Hi {{ subscriber.first_name | default: "there" }},', + 'export_sign_off' => 'Thanks', + ] + ); + + // Create Post. + $post_id = $this->factory->post->create( + [ + 'post_title' => 'Kit: Export: Greeting and Sign-off', + 'post_content' => '

Post content.

', + ] + ); + + // Export Post. + $result = $this->exporter->export_post_to_broadcast( $post_id ); + $this->assertNotInstanceOf(\WP_Error::class, $result); + + // Confirm the Greeting, content and Sign-off are in order. + $this->assertStringStartsWith( '

Hi {{ subscriber.first_name | default: "there" }},

', $this->request['content'] ); + $this->assertStringContainsString( '

Post content.

', $this->request['content'] ); + $this->assertStringEndsWith( '

Thanks

' . "\n", $this->request['content'] ); + } + + /** + * Test that disallowed HTML is removed from the Greeting and Sign-off settings when saved + * programmatically e.g. using MCP, and isn't included in the Broadcast. + * + * @since 3.4.8 + */ + public function testExportGreetingAndSignOffSanitizedOnSave() + { + // Define Greeting and Sign-off settings, including disallowed HTML. + $this->saveBroadcastsSettings( + [ + 'export_greeting' => 'Hi {{ subscriber.first_name }},', + 'export_sign_off' => 'Thanks', + ] + ); + + // Confirm the settings were stored without disallowed HTML. + $settings = new \ConvertKit_Settings_Broadcasts(); + $this->assertEquals( 'Hi {{ subscriber.first_name }},alert(1)', $settings->export_greeting() ); + $this->assertEquals( 'Thanksalert(2)', $settings->export_sign_off() ); + + // Create Post. + $post_id = $this->factory->post->create( + [ + 'post_title' => 'Kit: Export: Greeting and Sign-off: Sanitized', + 'post_content' => '

Post content.

', + ] + ); + + // Export Post. + $result = $this->exporter->export_post_to_broadcast( $post_id ); + $this->assertNotInstanceOf(\WP_Error::class, $result); + + // Confirm no script tags are in the Broadcast. + $this->assertStringNotContainsString( 'request['content'] ); + } + + /** + * Test that a link to read the Post on the web is added to the bottom or top of the Broadcast, + * with link text that can be changed using a filter. + * + * @since 3.4.8 + */ + public function testExportWebLink() + { + // Create Post. + $post_id = $this->factory->post->create( + [ + 'post_title' => 'Kit: Export: Web Link', + 'post_content' => '

Post content.

', + ] + ); + $link = '

Read this on the web

'; + + // Confirm the link is added to the bottom. + $this->saveBroadcastsSettings( [ 'export_web_link' => 'bottom' ] ); + $this->exporter->export_post_to_broadcast( $post_id ); + $this->assertStringEndsWith( $link, $this->request['content'] ); + + // Confirm the link is added to the top. + $this->saveBroadcastsSettings( [ 'export_web_link' => 'top' ] ); + $this->exporter->export_post_to_broadcast( $post_id ); + $this->assertStringStartsWith( $link, $this->request['content'] ); + + // Confirm the link text can be changed. + add_filter( + 'convertkit_broadcasts_exporter_web_link_text', + function () { + return 'View online'; + } + ); + $this->exporter->export_post_to_broadcast( $post_id ); + $this->assertStringStartsWith( '

View online

', $this->request['content'] ); + } + + /** + * Test that a link to read the Post on the web isn't added when the Post isn't published. + * + * @since 3.4.8 + */ + public function testExportWebLinkNotAddedToDraftPost() + { + // Define Read on the Web setting. + $this->saveBroadcastsSettings( [ 'export_web_link' => 'bottom' ] ); + + // Create draft Post. + $post_id = $this->factory->post->create( + [ + 'post_title' => 'Kit: Export: Web Link: Draft', + 'post_content' => '

Post content.

', + 'post_status' => 'draft', + ] + ); + + // Export Post. + $this->exporter->export_post_to_broadcast( $post_id ); + + // Confirm no link was added. + $this->assertStringNotContainsString( 'Read this on the web', $this->request['content'] ); + } + + /** + * Test that the Post's featured image is used as the Broadcast's thumbnail when the Make Public setting is enabled. + * + * @since 3.4.8 + */ + public function testExportFeaturedImageAsThumbnail() + { + // Enable Make Public setting. + $this->saveBroadcastsSettings( [ 'export_public' => 'on' ] ); + + // Create Post with a featured image. + $post_id = $this->createPostWithFeaturedImage( 'Kit: Export: Featured Image' ); + + // Export Post. + $result = $this->exporter->export_post_to_broadcast( $post_id ); + $this->assertNotInstanceOf(\WP_Error::class, $result); + + // Confirm the featured image is the Broadcast's thumbnail. + $this->assertEquals( wp_get_attachment_image_url( get_post_thumbnail_id( $post_id ), 'full' ), $this->request['thumbnail_url'] ); + $this->assertEquals( 'Featured image alt text', $this->request['thumbnail_alt'] ); + } + + /** + * Test that the Post's featured image isn't sent when the Make Public setting is disabled, + * as Kit only uses thumbnails for public Broadcasts. + * + * @since 3.4.8 + */ + public function testExportFeaturedImageNotUsedWhenNotPublic() + { + // Create Post with a featured image. + $post_id = $this->createPostWithFeaturedImage( 'Kit: Export: Featured Image: Not Public' ); + + // Export Post. + $result = $this->exporter->export_post_to_broadcast( $post_id ); + $this->assertNotInstanceOf(\WP_Error::class, $result); + + // Confirm no thumbnail was sent. + $this->assertArrayNotHasKey( 'thumbnail_url', $this->request ); + $this->assertArrayNotHasKey( 'thumbnail_alt', $this->request ); + } + + /** + * Creates a Post with a featured image that has alt text. + * + * @since 3.4.8 + * + * @param string $title Post title. + * @return int + */ + private function createPostWithFeaturedImage( $title ) + { + $post_id = $this->factory->post->create( + [ + 'post_title' => $title, + 'post_content' => '

Post content.

', + ] + ); + $image_id = $this->factory->attachment->create_upload_object( dirname( __DIR__ ) . '/Support/Data/icon.png', $post_id ); + update_post_meta( $image_id, '_wp_attachment_image_alt', 'Featured image alt text' ); + set_post_thumbnail( $post_id, $image_id ); + + return $post_id; + } + /** * Test that exporting a Post creates a Broadcast, storing its ID in the Post's meta. * diff --git a/tests/Integration/ResourceAssertionsTrait.php b/tests/Integration/ResourceAssertionsTrait.php new file mode 100644 index 000000000..3527b687c --- /dev/null +++ b/tests/Integration/ResourceAssertionsTrait.php @@ -0,0 +1,48 @@ +resource->order_by); + $this->assertCount(count($resources), $values); + + // Assert resources are in the expected order, using the same comparison as the resource class. + $sorted = $values; + usort($sorted, 'strcmp'); + if ($this->resource->order === 'desc') { + $sorted = array_reverse($sorted); + } + $this->assertEquals($sorted, $values); + } + + /** + * Asserts that the given resources are in the same order as the resource class' resources. + * + * @since 3.4.8 + * + * @param array $resources Resources. + */ + private function assertResourcesInOriginalOrder($resources) + { + $this->assertSame( + array_keys(array_intersect_key($this->resource->resources, $resources)), + array_keys($resources) + ); + } +} diff --git a/tests/Integration/ResourceCustomFieldsTest.php b/tests/Integration/ResourceCustomFieldsTest.php index b073fe558..cd3596a28 100644 --- a/tests/Integration/ResourceCustomFieldsTest.php +++ b/tests/Integration/ResourceCustomFieldsTest.php @@ -11,6 +11,8 @@ */ class ResourceCustomFieldsTest extends WPTestCase { + use ResourceAssertionsTrait; + /** * The testing implementation. * @@ -145,9 +147,8 @@ public function testGet() $this->assertArrayHasKey('id', reset($result)); $this->assertArrayHasKey('name', reset($result)); - // Assert order of data is in ascending alphabetical order. - $this->assertEquals('ck_field_1075083_url', reset($result)[ $this->resource->order_by ]); - $this->assertEquals('ck_field_276295_payment_method', end($result)[ $this->resource->order_by ]); + // Assert order of data matches the order_by and order properties. + $this->assertResourcesSorted($result); } /** @@ -176,9 +177,8 @@ public function testGetWithValidOrderByAndOrder() $this->assertArrayHasKey('id', reset($result)); $this->assertArrayHasKey('name', reset($result)); - // Assert order of data is in ascending alphabetical order. - $this->assertEquals('ck_field_276295_payment_method', reset($result)[ $this->resource->order_by ]); - $this->assertEquals('ck_field_1075083_url', end($result)[ $this->resource->order_by ]); + // Assert order of data matches the order_by and order properties. + $this->assertResourcesSorted($result); } /** @@ -207,8 +207,7 @@ public function testGetWithInvalidOrderBy() $this->assertArrayHasKey('name', reset($result)); // Assert order of data has not changed. - $this->assertEquals('ck_field_1075083_url', reset($result)['name']); - $this->assertEquals('ck_field_258240_notes', end($result)['name']); + $this->assertResourcesInOriginalOrder($result); } /** diff --git a/tests/Integration/ResourceFormsTest.php b/tests/Integration/ResourceFormsTest.php index fe695d5d1..fe24e197c 100644 --- a/tests/Integration/ResourceFormsTest.php +++ b/tests/Integration/ResourceFormsTest.php @@ -11,6 +11,8 @@ */ class ResourceFormsTest extends WPTestCase { + use ResourceAssertionsTrait; + /** * The testing implementation. * @@ -145,9 +147,8 @@ public function testGet() $this->assertArrayHasKey('id', reset($result)); $this->assertArrayHasKey('name', reset($result)); - // Assert order of data is in ascending alphabetical order. - $this->assertEquals('AAA Test', reset($result)[ $this->resource->order_by ]); - $this->assertEquals('WooCommerce Product Form', end($result)[ $this->resource->order_by ]); + // Assert order of data matches the order_by and order properties. + $this->assertResourcesSorted($result); } /** @@ -176,9 +177,8 @@ public function testGetWithValidOrderByAndOrder() $this->assertArrayHasKey('id', reset($result)); $this->assertArrayHasKey('name', reset($result)); - // Assert order of data is in ascending alphabetical order. - $this->assertEquals('WooCommerce Product Form', reset($result)[ $this->resource->order_by ]); - $this->assertEquals('AAA Test', end($result)[ $this->resource->order_by ]); + // Assert order of data matches the order_by and order properties. + $this->assertResourcesSorted($result); } /** @@ -207,8 +207,7 @@ public function testGetWithInvalidOrderBy() $this->assertArrayHasKey('name', reset($result)); // Assert order of data has not changed. - $this->assertEquals('WPForms Form', reset($result)['name']); - $this->assertEquals('Legacy Form', end($result)['name']); + $this->assertResourcesInOriginalOrder($result); } /** @@ -233,9 +232,8 @@ public function testGetNonInline() $this->assertArrayHasKey('id', reset($result)); $this->assertArrayHasKey('name', reset($result)); - // Assert order of data is in ascending alphabetical order. - $this->assertEquals($_ENV['CONVERTKIT_API_FORM_FORMAT_MODAL_NAME_ONLY'], reset($result)[ $this->resource->order_by ]); - $this->assertEquals($_ENV['CONVERTKIT_API_FORM_FORMAT_STICKY_BAR_NAME_ONLY'], end($result)[ $this->resource->order_by ]); + // Assert order of data matches the order_by and order properties. + $this->assertResourcesSorted($result); } /** @@ -264,9 +262,8 @@ public function testGetNonInlineWithValidOrderByAndOrder() $this->assertArrayHasKey('id', reset($result)); $this->assertArrayHasKey('name', reset($result)); - // Assert order of data is in ascending alphabetical order. - $this->assertEquals($_ENV['CONVERTKIT_API_FORM_FORMAT_STICKY_BAR_NAME_ONLY'], reset($result)[ $this->resource->order_by ]); - $this->assertEquals($_ENV['CONVERTKIT_API_FORM_FORMAT_MODAL_NAME_ONLY'], end($result)[ $this->resource->order_by ]); + // Assert order of data matches the order_by and order properties. + $this->assertResourcesSorted($result); } /** @@ -295,8 +292,7 @@ public function testGetNonInlineWithInvalidOrderBy() $this->assertArrayHasKey('name', reset($result)); // Assert order of data has not changed. - $this->assertEquals($_ENV['CONVERTKIT_API_FORM_FORMAT_STICKY_BAR_NAME_ONLY'], reset($result)['name']); - $this->assertEquals($_ENV['CONVERTKIT_API_FORM_FORMAT_MODAL_NAME_ONLY'], end($result)['name']); + $this->assertResourcesInOriginalOrder($result); } /** diff --git a/tests/Integration/ResourceLandingPagesTest.php b/tests/Integration/ResourceLandingPagesTest.php index 8ed0841b6..a1371090d 100644 --- a/tests/Integration/ResourceLandingPagesTest.php +++ b/tests/Integration/ResourceLandingPagesTest.php @@ -11,6 +11,8 @@ */ class ResourceLandingPagesTest extends WPTestCase { + use ResourceAssertionsTrait; + /** * The testing implementation. * @@ -145,9 +147,8 @@ public function testGet() $this->assertArrayHasKey('id', reset($result)); $this->assertArrayHasKey('name', reset($result)); - // Assert order of data is in ascending alphabetical order. - $this->assertEquals('Character Encoding', reset($result)[ $this->resource->order_by ]); - $this->assertEquals('Legacy Landing Page', end($result)[ $this->resource->order_by ]); + // Assert order of data matches the order_by and order properties. + $this->assertResourcesSorted($result); } /** @@ -176,9 +177,8 @@ public function testGetWithValidOrderByAndOrder() $this->assertArrayHasKey('id', reset($result)); $this->assertArrayHasKey('name', reset($result)); - // Assert order of data is in ascending alphabetical order. - $this->assertEquals('Legacy Landing Page', reset($result)[ $this->resource->order_by ]); - $this->assertEquals('Character Encoding', end($result)[ $this->resource->order_by ]); + // Assert order of data matches the order_by and order properties. + $this->assertResourcesSorted($result); } /** @@ -207,8 +207,7 @@ public function testGetWithInvalidOrderBy() $this->assertArrayHasKey('name', reset($result)); // Assert order of data has not changed. - $this->assertEquals('Character Encoding', reset($result)['name']); - $this->assertEquals('Legacy Landing Page', end($result)['name']); + $this->assertResourcesInOriginalOrder($result); } /** diff --git a/tests/Integration/ResourcePostsTest.php b/tests/Integration/ResourcePostsTest.php index ba2b2a25d..9a0ed53a9 100644 --- a/tests/Integration/ResourcePostsTest.php +++ b/tests/Integration/ResourcePostsTest.php @@ -11,6 +11,8 @@ */ class ResourcePostsTest extends WPTestCase { + use ResourceAssertionsTrait; + /** * The testing implementation. * @@ -289,9 +291,8 @@ public function testGet() $this->assertArrayHasKey('id', reset($result)); $this->assertArrayHasKey('title', reset($result)); - // Assert order of data is in descending published_at order. - $this->assertEquals('2024-04-30T08:00:36Z', reset($result)[ $this->resource->order_by ]); - $this->assertEquals('2022-01-24T00:00:00Z', end($result)[ $this->resource->order_by ]); + // Assert order of data matches the order_by and order properties. + $this->assertResourcesSorted($result); } /** @@ -320,9 +321,8 @@ public function testGetWithValidOrderByAndOrder() $this->assertArrayHasKey('id', reset($result)); $this->assertArrayHasKey('title', reset($result)); - // Assert order of data is in ascending alphabetical order. - $this->assertEquals('Broadcast 2', reset($result)[ $this->resource->order_by ]); - $this->assertEquals('Test Subject', end($result)[ $this->resource->order_by ]); + // Assert order of data matches the order_by and order properties. + $this->assertResourcesSorted($result); } /** @@ -351,8 +351,7 @@ public function testGetWithInvalidOrderBy() $this->assertArrayHasKey('title', reset($result)); // Assert order of data has not changed. - $this->assertEquals('2024-04-30T08:00:36Z', reset($result)['published_at']); - $this->assertEquals('2022-01-24T00:00:00Z', end($result)['published_at']); + $this->assertResourcesInOriginalOrder($result); } /** diff --git a/tests/Integration/ResourceProductsTest.php b/tests/Integration/ResourceProductsTest.php index 0a58351eb..156fe2e33 100644 --- a/tests/Integration/ResourceProductsTest.php +++ b/tests/Integration/ResourceProductsTest.php @@ -11,6 +11,8 @@ */ class ResourceProductsTest extends WPTestCase { + use ResourceAssertionsTrait; + /** * The testing implementation. * @@ -145,9 +147,8 @@ public function testGet() $this->assertArrayHasKey('id', reset($result)); $this->assertArrayHasKey('name', reset($result)); - // Assert order of data is in ascending alphabetical order. - $this->assertEquals('Example Tip Jar', reset($result)[ $this->resource->order_by ]); - $this->assertEquals('PDF Guide', end($result)[ $this->resource->order_by ]); + // Assert order of data matches the order_by and order properties. + $this->assertResourcesSorted($result); } /** @@ -176,9 +177,8 @@ public function testGetWithValidOrderByAndOrder() $this->assertArrayHasKey('id', reset($result)); $this->assertArrayHasKey('name', reset($result)); - // Assert order of data is in ascending alphabetical order. - $this->assertEquals('PDF Guide', reset($result)[ $this->resource->order_by ]); - $this->assertEquals('Example Tip Jar', end($result)[ $this->resource->order_by ]); + // Assert order of data matches the order_by and order properties. + $this->assertResourcesSorted($result); } /** @@ -207,8 +207,7 @@ public function testGetWithInvalidOrderBy() $this->assertArrayHasKey('name', reset($result)); // Assert order of data has not changed. - $this->assertEquals('PDF Guide', reset($result)['name']); - $this->assertEquals('Newsletter Subscription', end($result)['name']); + $this->assertResourcesInOriginalOrder($result); } /** diff --git a/tests/Integration/ResourceSequencesTest.php b/tests/Integration/ResourceSequencesTest.php index 4a967a8b7..eb0ce70fc 100644 --- a/tests/Integration/ResourceSequencesTest.php +++ b/tests/Integration/ResourceSequencesTest.php @@ -11,6 +11,8 @@ */ class ResourceSequencesTest extends WPTestCase { + use ResourceAssertionsTrait; + /** * The testing implementation. * @@ -145,9 +147,8 @@ public function testGet() $this->assertArrayHasKey('id', reset($result)); $this->assertArrayHasKey('name', reset($result)); - // Assert order of data is in ascending alphabetical order. - $this->assertEquals('Another Sequence', reset($result)[ $this->resource->order_by ]); - $this->assertEquals('WordPress Sequence', end($result)[ $this->resource->order_by ]); + // Assert order of data matches the order_by and order properties. + $this->assertResourcesSorted($result); } /** @@ -176,9 +177,8 @@ public function testGetWithValidOrderByAndOrder() $this->assertArrayHasKey('id', reset($result)); $this->assertArrayHasKey('name', reset($result)); - // Assert order of data is in ascending alphabetical order. - $this->assertEquals('WordPress Sequence', reset($result)[ $this->resource->order_by ]); - $this->assertEquals('Another Sequence', end($result)[ $this->resource->order_by ]); + // Assert order of data matches the order_by and order properties. + $this->assertResourcesSorted($result); } /** @@ -207,8 +207,7 @@ public function testGetWithInvalidOrderBy() $this->assertArrayHasKey('name', reset($result)); // Assert order of data has not changed. - $this->assertEquals('Another Sequence', reset($result)['name']); - $this->assertEquals('WordPress Sequence', end($result)['name']); + $this->assertResourcesInOriginalOrder($result); } /** diff --git a/tests/Integration/ResourceTagsTest.php b/tests/Integration/ResourceTagsTest.php index a8609365b..ed9f4d64b 100644 --- a/tests/Integration/ResourceTagsTest.php +++ b/tests/Integration/ResourceTagsTest.php @@ -11,6 +11,8 @@ */ class ResourceTagsTest extends WPTestCase { + use ResourceAssertionsTrait; + /** * The testing implementation. * @@ -145,9 +147,8 @@ public function testGet() $this->assertArrayHasKey('id', reset($result)); $this->assertArrayHasKey('name', reset($result)); - // Assert order of data is in ascending alphabetical order. - $this->assertEquals('gravityforms-tag-1', reset($result)[ $this->resource->order_by ]); - $this->assertEquals('wpforms', end($result)[ $this->resource->order_by ]); + // Assert order of data matches the order_by and order properties. + $this->assertResourcesSorted($result); } /** @@ -176,9 +177,8 @@ public function testGetWithValidOrderByAndOrder() $this->assertArrayHasKey('id', reset($result)); $this->assertArrayHasKey('name', reset($result)); - // Assert order of data is in ascending alphabetical order. - $this->assertEquals('wpforms', reset($result)[ $this->resource->order_by ]); - $this->assertEquals('gravityforms-tag-1', end($result)[ $this->resource->order_by ]); + // Assert order of data matches the order_by and order properties. + $this->assertResourcesSorted($result); } /** @@ -207,8 +207,7 @@ public function testGetWithInvalidOrderBy() $this->assertArrayHasKey('name', reset($result)); // Assert order of data has not changed. - $this->assertEquals('wpforms', reset($result)['name']); - $this->assertEquals('wordpress', end($result)['name']); + $this->assertResourcesInOriginalOrder($result); } /** diff --git a/tests/Integration/_bootstrap.php b/tests/Integration/_bootstrap.php index b3d9bbc7f..8271db9b3 100644 --- a/tests/Integration/_bootstrap.php +++ b/tests/Integration/_bootstrap.php @@ -1 +1,9 @@ checkSelectOptionOrder($I, $selectElement, $options); + // Form options include the format e.g. [inline], which isn't part of the sorted name. + $I->checkSelectResourceOptionsSorted($I, $selectElement, $prependOptions, true); } /** @@ -820,22 +809,7 @@ public function checkSelectFormOptionOrder($I, $selectElement, $prependOptions = */ public function checkSelectLandingPageOptionOrder($I, $selectElement, $prependOptions = false) { - // Define options. - // Legacy Landing Pages are hidden from dropdowns (as of the legacy - // landing page dropdown removal work), so `Landing Page` is now the - // last alphabetical entry rather than `Legacy Landing Page`. - $options = [ - 'Character Encoding', // First item. - 'Landing Page', // Last item. - ]; - - // Prepend options, such as 'Default' and 'None' to the options, if required. - if ( $prependOptions ) { - $options = array_merge( $prependOptions, $options ); - } - - // Check order. - $I->checkSelectOptionOrder($I, $selectElement, $options); + $I->checkSelectResourceOptionsSorted($I, $selectElement, $prependOptions); } /** @@ -850,19 +824,44 @@ public function checkSelectLandingPageOptionOrder($I, $selectElement, $prependOp */ public function checkSelectTagOptionOrder($I, $selectElement, $prependOptions = false) { - // Define options. - $options = [ - 'gravityforms-tag-1', // First item. - 'wpforms', // Last item. - ]; + $I->checkSelectResourceOptionsSorted($I, $selectElement, $prependOptions); + } + + /** + * Helper method to determine that the resources in the given select element are in + * ascending alphabetical order, after any prepended options, regardless of which + * resources exist on the Kit account. + * + * @since 3.4.8 + * + * @param EndToEndTester $I EndToEndTester. + * @param string $selectElement