diff --git a/CHANGELOG.md b/CHANGELOG.md index 1862dee..09b4897 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,21 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added + +* Added "hourly" as a default interval for stores ([#20]). +* Added new placeholders to user-facing messaging ([#20]): + - `{current_interval:date}` (alias of `{current_interval}`) + - `{current_interval:time}` + - `{next_interval:date}` (alias of `{next_interval}`) + - `{next_interval:time}` + +### Updated + +* The settings screen will now show custom placeholders that have been registered via the "limit_orders_message_placeholders" filter ([#20]). + ## [Version 1.1.2] - 2020-04-17 ### Fixed @@ -44,3 +59,4 @@ Initial plugin release. [#8]: https://github.com/nexcess/limit-orders/pull/8 [#10]: https://github.com/nexcess/limit-orders/pull/10 [#13]: https://github.com/nexcess/limit-orders/pull/13 +[#20]: https://github.com/nexcess/limit-orders/pull/20 diff --git a/README.md b/README.md index d24a44b..8a2ea9b 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ Configuration for Limit Orders for WooCommerce is available through WooCommerce
'; if ( current_user_can( 'manage_options' ) ) { @@ -78,13 +91,13 @@ public function admin_notice() { /* Translators: %1$s is the settings page URL, %2$s is the reset date for order limiting. */ __( 'Based on your store\'s configuration, new orders have been put on hold until %2$s.', 'limit-orders' ), $this->get_settings_url(), - $this->limiter->get_next_interval_start()->format( get_option( 'date_format' ) ) + $next ) ); } else { echo esc_html( sprintf( /* Translators: %1$s is the reset date for order limiting. */ __( 'Based on your store\'s configuration, new orders have been put on hold until %1$s.', 'limit-orders' ), - $this->limiter->get_next_interval_start()->format( get_option( 'date_format' ) ) + $next ) ); } echo '
' . __( 'Customize the messages shown to customers once ordering is disabled.', 'limit-orders' ) . '
' . __( 'Available placeholders: {limit}, {current_interval}, {next_interval}.', 'limit-orders' ) . '
', + 'desc' => '' . __( 'Customize the messages shown to customers once ordering is disabled.', 'limit-orders' ) . '
' . $available_placeholders ? '' . $available_placeholders . '
' : '', ], [ 'id' => OrderLimiter::OPTION_KEY . '[customer_notice]', @@ -116,6 +126,7 @@ protected function get_intervals() { global $wp_locale; $intervals = [ + 'hourly' => _x( 'Hourly (resets at the top of every hour)', 'order threshold interval', 'limit-orders' ), 'daily' => _x( 'Daily (resets every day)', 'order threshold interval', 'limit-orders' ), 'weekly' => sprintf( /* Translators: %1$s is the first day of the week, based on site configuration. */ diff --git a/tests/AdminTest.php b/tests/AdminTest.php index 6e39c27..6158ebb 100644 --- a/tests/AdminTest.php +++ b/tests/AdminTest.php @@ -26,7 +26,6 @@ public function admin_notices_should_not_be_shown_if_no_limits_have_been_reached $limiter = $this->getMockBuilder( OrderLimiter::class ) ->setMethods( [ 'has_reached_limit' ] ) ->getMock(); - $limiter->method( 'has_reached_limit' ) ->willReturn( false ); @@ -48,7 +47,6 @@ public function admin_notices_should_be_shown_once_limits_are_reached() { $limiter = $this->getMockBuilder( OrderLimiter::class ) ->setMethods( [ 'has_reached_limit' ] ) ->getMock(); - $limiter->method( 'has_reached_limit' ) ->willReturn( true ); @@ -71,7 +69,6 @@ public function admin_notices_should_not_include_links_to_settings_for_non_admin $limiter = $this->getMockBuilder( OrderLimiter::class ) ->setMethods( [ 'has_reached_limit' ] ) ->getMock(); - $limiter->method( 'has_reached_limit' ) ->willReturn( true ); @@ -81,4 +78,55 @@ public function admin_notices_should_not_include_links_to_settings_for_non_admin $this->assertNotContains( admin_url( 'admin.php?page=wc-settings' ), $output ); } + + /** + * @test + * @group Intervals + * @ticket https://github.com/nexcess/limit-orders/issues/18 + */ + public function intervals_of_less_than_a_day_should_use_time_instead_of_date_in_the_admin_notice() { + wp_set_current_user( $this->factory->user->create( [ + 'role' => 'editor', + ] ) ); + + $next = ( new \DateTime( 'now' ) )->setTime( 7, 0, 0 ); + $limiter = $this->getMockBuilder( OrderLimiter::class ) + ->setMethods( [ 'has_reached_limit', 'get_next_interval_start' ] ) + ->getMock(); + $limiter->method( 'has_reached_limit' ) + ->willReturn( true ); + $limiter->method( 'get_next_interval_start' ) + ->willReturn( $next ); + + ob_start(); + ( new Admin( $limiter ) )->admin_notice(); + $output = ob_get_clean(); + + $this->assertContains( $next->format( get_option( 'time_format' ) ), $output ); + } + + /** + * @test + * @group Intervals + */ + public function admin_notices_should_use_midnight_instead_of_dates_for_daily_interval() { + wp_set_current_user( $this->factory->user->create( [ + 'role' => 'editor', + ] ) ); + + $next = ( new \DateTime( 'now' ) )->setTime( 24, 0, 0 ); // Midnight. + $limiter = $this->getMockBuilder( OrderLimiter::class ) + ->setMethods( [ 'has_reached_limit', 'get_next_interval_start' ] ) + ->getMock(); + $limiter->method( 'has_reached_limit' ) + ->willReturn( true ); + $limiter->method( 'get_next_interval_start' ) + ->willReturn( $next ); + + ob_start(); + ( new Admin( $limiter ) )->admin_notice(); + $output = ob_get_clean(); + + $this->assertContains( __( 'midnight' ), $output ); + } } diff --git a/tests/OrderLimiterTest.php b/tests/OrderLimiterTest.php index ca00e8d..c41863d 100644 --- a/tests/OrderLimiterTest.php +++ b/tests/OrderLimiterTest.php @@ -29,12 +29,22 @@ public function reset_wc_notices() { /** * @test * @testdox get_interval() should return the interval setting + * @group Intervals */ public function get_interval_should_return_the_interval_setting() { update_option( OrderLimiter::OPTION_KEY, [ - 'interval' => 'daily', + 'interval' => 'weekly', ] ); + $this->assertSame( 'weekly', ( new OrderLimiter )->get_interval() ); + } + + /** + * @test + * @testdox get_interval() should default to "daily" + * @group Intervals + */ + public function get_interval_should_default_to_daily() { $this->assertSame( 'daily', ( new OrderLimiter )->get_interval() ); } @@ -108,12 +118,15 @@ public function get_message_should_not_expose_other_settings() { /** * @test * @testdox get_message() should replace the {current_interval} placeholder + * @testWith ["{current_interval}"] + * ["{current_interval:date}"] + * @group Placeholders */ - public function get_message_should_replace_current_interval_placeholder() { + public function get_message_should_replace_current_interval_placeholder( $placeholder ) { update_option( 'date_format', 'F j, Y' ); update_option( OrderLimiter::OPTION_KEY, [ 'interval' => 'weekly', - 'customer_notice' => 'This started on {current_interval}', + 'customer_notice' => "This started on {$placeholder}", ] ); $now = new \DateTimeImmutable( 'now', wp_timezone() ); @@ -125,9 +138,31 @@ public function get_message_should_replace_current_interval_placeholder() { ); } + /** + * @test + * @testdox get_message() should replace the {current_interval:time} placeholder + * @group Placeholders + */ + public function get_message_should_replace_current_interval_time_placeholder() { + update_option( 'time_format', 'g:ia' ); + update_option( OrderLimiter::OPTION_KEY, [ + 'interval' => 'hourly', + 'customer_notice' => "This started at {current_interval:time}", + ] ); + + $now = new \DateTimeImmutable( 'now', wp_timezone() ); + $limiter = new OrderLimiter( $now ); + + $this->assertSame( + 'This started at ' . $limiter->get_interval_start()->format( 'g:ia' ), + $limiter->get_message( 'customer_notice' ) + ); + } + /** * @test * @testdox get_message() should replace the {limit} placeholder + * @group Placeholders */ public function get_message_should_replace_limit_placeholder() { update_option( OrderLimiter::OPTION_KEY, [ @@ -145,12 +180,15 @@ public function get_message_should_replace_limit_placeholder() { /** * @test * @testdox get_message() should replace the {next_interval} placeholder + * @testWith ["{next_interval}"] + * ["{next_interval:date}"] + * @group Placeholders */ - public function get_message_should_replace_next_interval_placeholder() { + public function get_message_should_replace_next_interval_placeholder( $placeholder ) { update_option( 'date_format', 'F j, Y' ); update_option( OrderLimiter::OPTION_KEY, [ 'interval' => 'weekly', - 'customer_notice' => 'Check back on {next_interval}', + 'customer_notice' => "Check back on {$placeholder}", ] ); $now = new \DateTimeImmutable( 'now', wp_timezone() ); @@ -162,6 +200,87 @@ public function get_message_should_replace_next_interval_placeholder() { ); } + /** + * @test + * @testdox get_message() should replace the {next_interval:time} placeholder + * @group Placeholders + */ + public function get_message_should_replace_next_interval_time_placeholder() { + update_option( 'time_format', 'g:ia' ); + update_option( OrderLimiter::OPTION_KEY, [ + 'interval' => 'hourly', + 'customer_notice' => "Check back at {next_interval:time}", + ] ); + + $now = new \DateTimeImmutable( 'now', wp_timezone() ); + $limiter = new OrderLimiter( $now ); + + $this->assertSame( + 'Check back at ' . $limiter->get_next_interval_start()->format( 'g:ia' ), + $limiter->get_message( 'customer_notice' ) + ); + } + + /** + * @test + * @group Placeholders + */ + public function get_placeholders_should_return_an_array_of_default_placeholders() { + update_option( 'date_format', 'F j, Y' ); + update_option( 'time_format', 'g:ia' ); + update_option( OrderLimiter::OPTION_KEY, [ + 'interval' => 'hourly', + ] ); + + $now = new \DateTimeImmutable( '2020-04-27 12:15:00', wp_timezone() ); + $current = new \DateTimeImmutable( '2020-04-27 12:00:00', wp_timezone() ); + $next = new \DateTimeImmutable( '2020-04-27 13:00:00', wp_timezone() ); + $placeholders = ( new OrderLimiter( $now ) )->get_placeholders(); + + $this->assertSame( $current->format( 'F j, Y' ), $placeholders['{current_interval}'] ); + $this->assertSame( $current->format( 'F j, Y' ), $placeholders['{current_interval:date}'] ); + $this->assertSame( $current->format( 'g:ia' ), $placeholders['{current_interval:time}'] ); + $this->assertSame( $next->format( 'F j, Y' ), $placeholders['{next_interval}'] ); + $this->assertSame( $next->format( 'F j, Y' ), $placeholders['{next_interval:date}'] ); + $this->assertSame( $next->format( 'g:ia' ), $placeholders['{next_interval:time}'] ); + } + + /** + * @test + * @group Placeholders + */ + public function time_placeholders_should_replace_00_with_midnight() { + $this->markTestIncomplete( 'https://github.com/nexcess/limit-orders/issues/21' ); + + update_option( OrderLimiter::OPTION_KEY, [ + 'interval' => 'daily', + ] ); + + $now = new \DateTimeImmutable( '2020-04-27 12:15:00', wp_timezone() ); + $current = new \DateTimeImmutable( '2020-04-27 00:00:00', wp_timezone() ); + $next = new \DateTimeImmutable( '2020-04-28 00:00:00', wp_timezone() ); + $placeholders = ( new OrderLimiter( $now ) )->get_placeholders(); + + $this->assertSame( __( 'midnight', 'limit-orders' ), $placeholders['{current_interval:time}'] ); + $this->assertSame( __( 'midnight', 'limit-orders' ), $placeholders['{next_interval:time}'] ); + } + + /** + * @test + * @group Placeholders + */ + public function get_placeholders_should_filter_placeholders() { + add_filter( 'limit_orders_message_placeholders', function ( $placeholders ) { + $placeholders['{test}'] = 'Test value'; + + return $placeholders; + } ); + + $placeholders = ( new OrderLimiter() )->get_placeholders(); + + $this->assertSame( 'Test value', $placeholders['{test}'] ); + } + /** * @test * @testdox get_remaining_orders() should return the number of orders left for the interval @@ -206,6 +325,27 @@ public function get_remaining_orders_should_return_zero_if_limits_are_met_or_exc /** * @test + * @group Intervals + * @ticket https://github.com/nexcess/limit-orders/issues/18 + */ + public function get_interval_start_for_hourly() { + update_option( OrderLimiter::OPTION_KEY, [ + 'interval' => 'hourly', + ] ); + + $now = new \DateTimeImmutable( '2020-04-27 12:05:00', wp_timezone() ); + $start = new \DateTimeImmutable( '2020-04-27 12:00:00', wp_timezone() ); + + $this->assertSame( + $start->format( 'r' ), + ( new OrderLimiter( $now ) )->get_interval_start()->format( 'r' ), + 'Hourly intervals should start at the top of the hour.' + ); + } + + /** + * @test + * @group Intervals */ public function get_interval_start_for_daily() { update_option( OrderLimiter::OPTION_KEY, [ @@ -224,6 +364,7 @@ public function get_interval_start_for_daily() { /** * @test + * @group Intervals */ public function get_interval_start_for_weekly() { update_option( 'week_starts_on', 1 ); @@ -244,6 +385,7 @@ public function get_interval_start_for_weekly() { /** * @test + * @group Intervals */ public function get_interval_start_for_weekly_with_a_non_standard_day() { update_option( 'week_starts_on', 6 ); @@ -264,6 +406,7 @@ public function get_interval_start_for_weekly_with_a_non_standard_day() { /** * @test + * @group Intervals */ public function get_interval_start_for_weekly_when_today_is_the_first_day_of_the_week() { update_option( 'week_starts_on', 1 ); @@ -284,6 +427,7 @@ public function get_interval_start_for_weekly_when_today_is_the_first_day_of_the /** * @test + * @group Intervals */ public function get_interval_start_for_monthly() { $today = new \DateTimeImmutable( 'now', wp_timezone() ); @@ -301,6 +445,7 @@ public function get_interval_start_for_monthly() { /** * @test + * @group Intervals */ public function get_interval_start_should_be_idempotent() { $now = new \DateTimeImmutable( '00:00:00', wp_timezone() ); @@ -320,6 +465,27 @@ public function get_interval_start_should_be_idempotent() { /** * @test + * @group Intervals + * @ticket https://github.com/nexcess/limit-orders/issues/18 + */ + public function get_next_interval_start_for_hourly() { + update_option( OrderLimiter::OPTION_KEY, [ + 'interval' => 'hourly', + ] ); + + $now = new \DateTimeImmutable( '2020-04-27 12:05:00', wp_timezone() ); + $next = new \DateTimeImmutable( '2020-04-27 13:00:00', wp_timezone() ); + + $this->assertSame( + $next->format( 'r' ), + ( new OrderLimiter( $now ) )->get_next_interval_start()->format( 'r' ), + 'The next hourly interval should begin at the top of the next hour.' + ); + } + + /** + * @test + * @group Intervals */ public function get_next_interval_start_for_daily() { update_option( OrderLimiter::OPTION_KEY, [ @@ -339,6 +505,7 @@ public function get_next_interval_start_for_daily() { /** * @test + * @group Intervals */ public function get_next_interval_start_for_weekly() { update_option( 'week_starts_on', 1 ); @@ -359,6 +526,7 @@ public function get_next_interval_start_for_weekly() { /** * @test + * @group Intervals */ public function get_next_interval_start_for_monthly() { update_option( OrderLimiter::OPTION_KEY, [ @@ -378,6 +546,27 @@ public function get_next_interval_start_for_monthly() { /** * @test + * @group Intervals + * @ticket https://github.com/nexcess/limit-orders/issues/18 + */ + public function get_seconds_until_next_interval_for_hourly() { + update_option( OrderLimiter::OPTION_KEY, [ + 'interval' => 'hourly', + ] ); + + $now = new \DateTimeImmutable( '2020-04-27 12:05:00', wp_timezone() ); + $next = new \DateTimeImmutable( '2020-04-27 13:00:00', wp_timezone() ); + + $this->assertSame( + $next->getTimestamp() - $now->getTimestamp(), + ( new OrderLimiter( $now ) )->get_seconds_until_next_interval(), + 'It should return the number of seconds until the next hour begins.' + ); + } + + /** + * @test + * @group Intervals */ public function get_seconds_until_next_interval_for_daily() { update_option( OrderLimiter::OPTION_KEY, [ @@ -397,6 +586,7 @@ public function get_seconds_until_next_interval_for_daily() { /** * @test + * @group Intervals */ public function get_seconds_until_next_interval_for_weekly() { update_option( 'week_starts_on', 1 ); @@ -417,6 +607,7 @@ public function get_seconds_until_next_interval_for_weekly() { /** * @test + * @group Intervals */ public function get_seconds_until_next_interval_for_monthly() { update_option( OrderLimiter::OPTION_KEY, [ @@ -641,8 +832,15 @@ public function the_transient_should_be_updated_each_time_an_order_is_placed() { /** * @test + * @ticket https://github.com/nexcess/limit-orders/pull/13 */ public function count_qualifying_orders_should_not_limit_results() { + update_option( OrderLimiter::OPTION_KEY, [ + 'enabled' => true, + 'interval' => 'daily', + 'limit' => 100, + ] ); + for ( $i = 0; $i < 24; $i++ ) { $this->generate_order(); } diff --git a/tests/SettingsTest.php b/tests/SettingsTest.php index 4e81942..f80bca4 100644 --- a/tests/SettingsTest.php +++ b/tests/SettingsTest.php @@ -30,6 +30,24 @@ public function the_options_should_be_added_to_their_own_page() { /** * @test + * @group Intervals + * @ticket https://github.com/nexcess/limit-orders/issues/18 + */ + public function it_should_include_default_intervals() { + $method = new \ReflectionMethod( Settings::class, 'get_intervals' ); + $method->setAccessible( true ); + + $intervals = $method->invoke( new Settings( new OrderLimiter() ) ); + + $this->assertArrayHasKey( 'daily', $intervals ); + $this->assertArrayHasKey( 'weekly', $intervals ); + $this->assertArrayHasKey( 'monthly', $intervals ); + $this->assertArrayHasKey( 'hourly', $intervals, 'Hourly was added in https://github.com/nexcess/limit-orders/issues/18' ); + } + + /** + * @test + * @group Intervals */ public function available_intervals_should_be_filterable() { $intervals = [ @@ -52,4 +70,23 @@ public function available_intervals_should_be_filterable() { $this->fail( 'Did not find setting with ID "'. OrderLimiter::OPTION_KEY . '[interval]".' ); } + + /** + * @test + * @group Placeholders + */ + public function available_placeholders_should_be_shown_in_the_messages_section() { + $limiter = new OrderLimiter(); + $sections = array_filter( ( new Settings( new OrderLimiter() ) )->get_settings(), function ( $section ) { + return 'limit-orders-messaging' === $section['id'] && 'title' === $section['type']; + } ); + + $this->assertCount( 1, $sections, 'Expected to see only one instance of "limit-orders-messaging".' ); + + $description = current( $sections )['desc']; + + foreach ( $limiter->get_placeholders() as $placeholder => $value ) { + $this->assertContains( '' . $placeholder . '', $description ); + } + } }