diff --git a/src/class-tiny-dashboard.php b/src/class-tiny-dashboard.php new file mode 100644 index 00000000..7a06f5bd --- /dev/null +++ b/src/class-tiny-dashboard.php @@ -0,0 +1,155 @@ +settings = $settings; + } + + public function admin_init() { + if ( current_user_can( 'upload_files' ) ) { + add_action( + 'wp_dashboard_setup', + array( $this, 'add_dashboard_widget' ) + ); + } + } + + public function add_dashboard_widget() { + wp_enqueue_style( + self::NAME . '_dashboard_widget', + plugins_url( '/css/dashboard-widget.css', __FILE__ ), + array(), + Tiny_Plugin::version() + ); + + wp_add_dashboard_widget( + $this->get_prefixed_name( 'dashboard_widget' ), + esc_html__( 'TinyPNG - JPEG, PNG & WebP image compression', 'tiny-compress-images' ), + array( $this, 'add_widget_view' ) + ); + } + + public function add_widget_view() { + $stats = Tiny_Bulk_Optimization::get_optimization_statistics( $this->settings ); + $widget = $this->get_widget_data( $stats ); + $email_address = $this->settings->get_email_address(); + include __DIR__ . '/views/dashboard-widget.php'; + } + + /** + * Logic for the view + * + * @param array $optimization_stats See Tiny_Bulk_Optimization::get_optimization_statistics(). + * @return array + */ + public function get_widget_data( $optimization_stats ) { + $images_total = max( 0, intval( $optimization_stats['uploaded-images'] ) ); + $images_remaining = min( + $images_total, + count( $optimization_stats['available-for-optimization'] ) + ); + $images_optimized = $images_total - $images_remaining; + $bytes_saved = $optimization_stats['unoptimized-library-size'] + - $optimization_stats['optimized-library-size']; + + $percentage = $images_total > 0 + ? intval( floor( $images_optimized / $images_total * 100 ) ) + : 0; + + $label = self::get_label_text( $percentage ); + + if ( 0 === $images_total ) { + $status = 'empty'; + $panda = 'panda-waiting.png'; + } elseif ( 0 === $images_remaining ) { + $status = 'done'; + $panda = 'panda-laying.png'; + } else { + $status = 'in_progress'; + $panda = 'panda-waiting.png'; + } + + // Only free accounts have a limited number of credits, including + // free accounts that bought a credit bundle. + $remaining_credits = $this->settings->get_remaining_credits(); + $has_limited_credits = $this->settings->is_on_free_plan() && + is_numeric( $remaining_credits ); + $remaining_credits = $has_limited_credits ? intval( $remaining_credits ) : null; + + if ( ! $this->settings->has_api_key() ) { + $notice = 'no_api_key'; + } elseif ( null !== $remaining_credits && + $remaining_credits < self::LOW_CREDITS_THRESHOLD ) { + $notice = 'low_credits'; + } else { + $notice = null; + } + + return array( + 'status' => $status, + 'images_optimized' => $images_optimized, + 'images_total' => $images_total, + 'images_remaining' => $images_remaining, + 'bytes_saved' => max( 0, intval( $bytes_saved ) ), + 'percentage' => $percentage, + 'label' => $label, + 'panda' => $panda, + 'remaining_credits' => $remaining_credits, + 'notice' => $notice, + ); + } + + /** + * Returns a description based on the percentage of optimized library + * + * @param int $percentage + * @return string label + */ + private static function get_label_text( $percentage ) { + if ( $percentage > 99 ) { + return __( 'optimized', 'tiny-compress-images' ); + } + if ( $percentage > 75 ) { + return __( 'almost there', 'tiny-compress-images' ); + } + + if ( $percentage > 50 ) { + return __( 'halfway there', 'tiny-compress-images' ); + } + + if ( 0 === $percentage ) { + return ''; + } + + return __( 'getting started', 'tiny-compress-images' ); + } +} diff --git a/src/class-tiny-plugin.php b/src/class-tiny-plugin.php index 49cd9718..c0751435 100644 --- a/src/class-tiny-plugin.php +++ b/src/class-tiny-plugin.php @@ -42,6 +42,7 @@ public function __construct() { parent::__construct(); $this->settings = new Tiny_Settings(); new Tiny_Conversion( $this->settings ); + new Tiny_Dashboard( $this->settings ); } public function set_compressor( $compressor ) { @@ -104,11 +105,6 @@ public function ajax_init() { $this->get_method( 'compress_image_for_bulk' ) ); - add_action( - 'wp_ajax_tiny_get_optimization_statistics', - $this->get_method( 'ajax_optimization_statistics' ) - ); - add_action( 'wp_ajax_tiny_get_compression_status', $this->get_method( 'ajax_compression_status' ) @@ -157,11 +153,6 @@ public function ajax_init() { } public function admin_init() { - add_action( - 'wp_dashboard_setup', - $this->get_method( 'add_dashboard_widget' ) - ); - add_action( 'admin_enqueue_scripts', $this->get_method( 'enqueue_scripts' ) @@ -668,15 +659,6 @@ public function compress_image_for_bulk() { exit(); } - public function ajax_optimization_statistics() { - if ( check_ajax_referer( 'tiny-compress', '_nonce', false ) && - current_user_can( 'upload_files' ) ) { - $stats = Tiny_Bulk_Optimization::get_optimization_statistics( $this->settings ); - echo json_encode( $stats ); - } - exit(); - } - public function ajax_compression_status() { $response = $this->validate_ajax_attachment_request(); @@ -819,60 +801,6 @@ public function render_bulk_optimization_page() { include __DIR__ . '/views/bulk-optimization.php'; } - public function add_dashboard_widget() { - wp_enqueue_style( - self::NAME . '_chart', - plugins_url( '/css/optimization-chart.css', __FILE__ ), - array(), - self::version() - ); - - wp_enqueue_style( - self::NAME . '_dashboard_widget', - plugins_url( '/css/dashboard-widget.css', __FILE__ ), - array(), - self::version() - ); - - wp_register_script( - self::NAME . '_dashboard_widget', - plugins_url( '/js/dashboard-widget.js', __FILE__ ), - array(), - self::version(), - true - ); - - /* - This might be deduplicated with the admin script localization, but - the order of including scripts is sometimes different. So in that - case we need to make sure that the order of inclusion is correct. */ - wp_localize_script( - self::NAME . '_dashboard_widget', - 'tinyCompressDashboard', - array( - 'nonce' => wp_create_nonce( 'tiny-compress' ), - ) - ); - - wp_enqueue_script( self::NAME . '_dashboard_widget' ); - - $widget_title = esc_html__( - 'TinyPNG Image Optimization – Image Compression JPEG & PNG | Convert to WebP & AVIF', - 'tiny-compress-images' - ); - - wp_add_dashboard_widget( - $this->get_prefixed_name( 'dashboard_widget' ), - $widget_title, - $this->get_method( 'add_widget_view' ) - ); - } - - public function add_widget_view() { - $admin_colors = self::retrieve_admin_colors(); - include __DIR__ . '/views/dashboard-widget.php'; - } - private static function retrieve_admin_colors() { global $_wp_admin_css_colors; $admin_colour_scheme = get_user_option( 'admin_color', get_current_user_id() ); diff --git a/src/class-tiny-settings.php b/src/class-tiny-settings.php index 8f442ea5..37f2cba7 100644 --- a/src/class-tiny-settings.php +++ b/src/class-tiny-settings.php @@ -224,6 +224,10 @@ protected function get_api_key() { } } + public function has_api_key() { + return (bool) $this->get_api_key(); + } + protected function get_api_key_pending() { if ( defined( 'TINY_API_KEY' ) ) { return false; diff --git a/src/css/admin.css b/src/css/admin.css index bad18a87..40b2a869 100644 --- a/src/css/admin.css +++ b/src/css/admin.css @@ -1,3 +1,7 @@ +:root { + --tiny-color-text-muted: #6a7282 +} + div.misc-pub-section.tiny-compress-images { border-top: 1px solid #e1e1e1; padding-bottom: 16px; diff --git a/src/css/dashboard-widget.css b/src/css/dashboard-widget.css index bf4b9a80..1aa76303 100644 --- a/src/css/dashboard-widget.css +++ b/src/css/dashboard-widget.css @@ -1,129 +1,153 @@ -div.tiny_dashboard_widget div.inside { - min-height: 200px; +#tinypng_dashboard_widget .inside { margin: 0; - padding-bottom: 0; - overflow: hidden; + padding: 0; + container-type: inline-size; } -div.tiny_dashboard_widget div.inside div.spinner { - position: absolute; - visibility: visible; - z-index: 10; - top: 30%; - left: 50%; - margin-left: -12px; +.tiny-widget { + display: flex; + min-height: 210px; + background: url("../images/grass.png") no-repeat right bottom / 398px 18px; + color: var(--tiny-color-text-muted); + font-size: 13px; + line-height: 17px; } -div.tiny_dashboard_widget div.inside div.sky { - position: absolute; - left: 0; - top: 0; - height: 100%; - width: 100%; - opacity: 0.3; - background: linear-gradient(180deg, #c6dde4 0%, #fefeff 100%); - /* ie6-9 */ filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#33C6DDE4', endColorstr='#33FEFEFF',GradientType=0 ); +.tiny-widget-main { + display: flex; + flex-shrink: 0; + flex-direction: column; + justify-content: space-between; } -div.tiny_dashboard_widget div.inside div.cloud { - position: absolute; - right: 0; - top: 0; - height: 240px; - width: 298px; - background-image: url("../images/cloud.png"); - background-size: cover; +.tiny-widget-content { + display: flex; + flex: 1; + align-items: center; + gap: 20px; + padding: 20px 0 20px 20px; } -div.tiny_dashboard_widget div.inside div.grass { - position: absolute; - right: 0; - bottom: 0; - height: 24px; - width: 536px; - background-image: url("../images/grass.png"); - background-size: cover; +.tiny-widget-stats { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 20px; } -div.tiny_dashboard_widget div.inside div.panda { - position: absolute; - bottom: 0; - background-repeat: no-repeat; - -webkit-background-size: 100%; - background-size: cover; - background-position: bottom; +.tiny-widget-summary { + display: flex; + flex-direction: column; + gap: 4px; } -div.tiny_dashboard_widget.full-optimized div.inside div.panda { - right: 10%; - width: 305px; - height: 105.5px; - margin-right: 0; - background-image: url("../images/panda-laying.png"); +.tiny-widget-title { + color: #030712; + font-size: 16px; + white-space: nowrap; } -div.tiny_dashboard_widget.half-optimized div.inside div.panda { - right: -230px; - width: 255px; - height: 203px; - margin-right: 45%; - background-image: url("../images/panda-eating.png"); +.tiny-widget-meta { + display: flex; + gap: 10px; + white-space: nowrap; + color: #989898; + font-size: 11px; } -div.tiny_dashboard_widget.not-optimized div.inside div.panda, -div.tiny_dashboard_widget.no-images-uploaded div.inside div.panda { - right: -196px; - width: 241px; - height: 203px; - margin-right: 45%; - background-image: url("../images/panda-waiting.png"); +.tiny-widget-meta span { + display: inline-flex; + align-items: center; + gap: 4px; } -div.tiny_dashboard_widget.no-images-uploaded div.inside div.no-images, -div.tiny_dashboard_widget.not-optimized div.inside div.not-optimized, -div.tiny_dashboard_widget.half-optimized div.inside div.half-optimized, -div.tiny_dashboard_widget.full-optimized div.inside div.full-optimized { - display: block !important; - position: relative; - z-index: 1; +.tiny-widget-meta svg { + opacity: 0.5; } -div.tiny_dashboard_widget div.inside div#optimization-chart { - margin-top: 12px; - margin-bottom: 40px; - min-width: 160px; +a.tiny-widget-bulk-button { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 10px; + width: 159px; + height: 37px; + background: #3858e9; + color: #fff; + font-size: 13px; + font-weight: bold; + line-height: normal; + text-decoration: none; } -div.tiny_dashboard_widget.full-optimized div.inside.mobile div#optimization-chart { - width: 100%; - margin-top: -5px; - margin-bottom: 95px; +a.tiny-widget-bulk-button:hover, +a.tiny-widget-bulk-button:focus { + background: #2145e6; + color: #fff; } -.ie8 div.tiny_dashboard_widget div.inside div.panda { - background: none; +.tiny-widget-nav { + display: flex; + align-items: center; + padding: 10px 20px; + font-size: 12px; + font-weight: bold; } -.ie8 div.tiny_dashboard_widget div.inside div.grass { - width: 100%; - background-image: url("../images/grass-ie8.png"); - background-repeat: no-repeat; - background-position: bottom; +.tiny-widget-nav a { + color: #3858e9; + text-decoration: none; } -.ie8 div.tiny_dashboard_widget div.inside div#optimization-chart { - display: none; +.tiny-widget-nav a + a { + margin-left: 10px; + padding-left: 10px; + border-left: 1px solid #ccc; } -.ie8 div.tiny_dashboard_widget.no-images-uploaded div.inside div#ie8-compressed, -.ie8 div.tiny_dashboard_widget.not-optimized div.inside div#ie8-compressed, -.ie8 div.tiny_dashboard_widget.half-optimized div.inside div#ie8-compressed, -.ie8 div.tiny_dashboard_widget.full-optimized div.inside div#ie8-compressed { - display: block !important; - position: relative; - z-index: 1; +.tiny-widget-aside { + display: flex; + flex: 1; + flex-direction: column; + align-items: flex-end; + justify-content: flex-end; + gap: 10px; + min-width: 0; + padding: 20px 12px 0 20px; } -#tinypng_dashboard_widget > button.handlediv { - margin-top: 0; +a.tiny-widget-notice { + display: block; + padding: 10px; + border-radius: 4px; + background: #fef3e8; + color: #40444f; + font-size: 11px; + text-decoration: none; + text-align: center; +} + +a.tiny-widget-notice:hover, +a.tiny-widget-notice:focus { + background: #fde6cf; + color: #40444f; +} + +.tiny-widget-notice-api-key { + text-align: center; +} + +.tiny-widget-panda { + width: 179px; + height: auto; +} + +@container (max-width: 560px) { + .tiny-widget { + background: none; + } + + .tiny-widget-aside { + display: none; + } } diff --git a/src/js/dashboard-widget.js b/src/js/dashboard-widget.js deleted file mode 100644 index 0bd39aa1..00000000 --- a/src/js/dashboard-widget.js +++ /dev/null @@ -1,125 +0,0 @@ -(function() { - function generateDashboardWidget(element) { - element = jQuery(element); - var container = element.find('.inside'); - jQuery('.chart').addClass('hidden'); - // Adding a class to the widget element so that classes are only used in the stylesheet - jQuery('#tinypng_dashboard_widget').addClass('tiny_dashboard_widget'); - attachHandlers(container); - retrieveStats(container); - } - - function retrieveStats(container) { - jQuery.ajax({ - url: ajaxurl, - type: 'POST', - data: { - _nonce: tinyCompressDashboard.nonce, - action: 'tiny_get_optimization_statistics', - id: '#tinypng_dashboard_widget' - }, - success: function(data) { - if (data === 0) { - container.append('

An error occured.

'); - } else { - renderWidget(data); - } - }, - error: function() { - container.append('

An error occured.

'); - } - }); - } - - function attachHandlers(container) { - setContainerClass(container); - jQuery(window).resize(function(){ - setContainerClass(container); - }); - jQuery('#tinypng_dashboard_widget .hndle').click(function() { - jQuery(this).siblings('.inside').removeClass('mobile'); - }); - } - - function setContainerClass(container) { - if (jQuery(container).width() < 400) { - jQuery(container).addClass('mobile'); - } else if (jQuery(container).width() < 490 && jQuery(container).width() >= 400) { - jQuery(container).addClass('tablet'); - jQuery(container).removeClass('mobile'); - } else if (jQuery(container).width() >= 490) { - jQuery(container).removeClass('tablet').removeClass('mobile'); - } - } - - function renderWidget(data) { - var stats = jQuery.parseJSON(data); - if (stats !== null) { - var savings = stats['display-percentage']; - var libraryOptimized = optimizedPercentage(stats); - renderContent(libraryOptimized, stats, savings); - renderChart(savings); - jQuery('#optimization-chart').show(); - } - jQuery('#widget-spinner').attr('class', 'hidden'); - } - - function renderPercentage(percentage) { - jQuery('#savings-percentage').find('span').html(percentage); - } - - function renderContent(percentage, stats, savingsPercentage) { - renderPercentage(savingsPercentage); - if ( 0 === stats['uploaded-images'] + stats['available-unoptimized-sizes'] ) { - jQuery('#tinypng_dashboard_widget').addClass('no-images-uploaded'); - } else if ( percentage === 0 ) { - jQuery('#tinypng_dashboard_widget').addClass('not-optimized'); - } else if ( percentage === 100 ) { - jQuery('#tinypng_dashboard_widget').addClass('full-optimized'); - } else { - jQuery('#uploaded-images').html( stats['uploaded-images'] ); - jQuery('#unoptimized-sizes').html( stats['available-unoptimized-sizes'] ); - jQuery('#tinypng_dashboard_widget').addClass('half-optimized'); - } - jQuery('#ie8-compressed').find('span').html(savingsPercentage); - } - - function chartOptions(percentage) { - var chart = {}; - chart.size = 160; - chart.radius = chart.size / 2 * 0.9; - chart['main-radius'] = chart.radius * 0.88; - chart['circle-size'] = 2 * Math.PI * chart['main-radius']; - chart['dash-array-size'] = percentage / 100 * chart['circle-size']; - return chart; - } - - function optimizedPercentage(stats) { - if ( 0 !== stats['unoptimized-library-size'] ) { - return Math.round((stats['optimized-image-sizes'] / (stats['optimized-image-sizes'] + stats['available-unoptimized-sizes']) * 100), 0); - } else { - return 0; - } - } - - function renderChart(savingsPercentage) { - var chart = chartOptions(savingsPercentage); - jQuery('#optimization-chart svg circle.main').css('stroke-dasharray', chart['dash-array-size'] + ' ' + chart['circle-size']); - var style = - ' @keyframes shwoosh {' + - ' from { stroke-dasharray: 0 ' + chart['circle-size'] + '}' + - ' to { stroke-dasharray:' + chart['dash-array-size'] + ' ' + chart['circle-size'] + '}}'; - - // JQuery bug where you cannot append to style tag https://bugs.jquery.com/ticket/9832 - try { - jQuery('#tinypng_dashboard_widget .inside style').append(style); - } catch(err) { - - } - } - - // Check if widget is loaded - if (jQuery('#tinypng_dashboard_widget').length) { - generateDashboardWidget('#tinypng_dashboard_widget'); - } -}).call(); diff --git a/src/views/dashboard-widget.php b/src/views/dashboard-widget.php index 5cc866b6..f7e2b086 100644 --- a/src/views/dashboard-widget.php +++ b/src/views/dashboard-widget.php @@ -3,102 +3,112 @@ /** * Dashboard Widget * - * @var array $admin_colors + * @var array $widget See Tiny_Dashboard::get_widget_data(). + * @var string $email_address * */ -$link = '' . esc_html__( 'bulk optimization', 'tiny-compress-images' ) . ''; +$percentage = $widget['percentage']; +$label = $widget['label']; + +$bulk_url = admin_url( 'upload.php?page=tiny-bulk-optimization' ); +$settings_url = admin_url( 'options-general.php?page=tinify' ); +$upgrade_url = 'https://tinypng.com/dashboard/api?type=upgrade&mail=' . + str_replace( '%20', '%2B', rawurlencode( $email_address ) ) . + '&utm_source=wordpress-plugin&utm_medium=referral&utm_campaign=upgrade&utm_content=dashboard-widget'; +$strong = array( 'strong' => array() ); +$database_icon = ''; ?> - -
-
-
-
-
-
-

-
-
-

- friendly_user_name() ) ); - echo ' '; - printf( - wp_kses( - /* translators: %s: bulk optimization page */ - __( 'If you like you can to optimize your whole library in one go with the %s page.', 'tiny-compress-images' ), - array( - 'a' => array( - 'href' => array(), - ), - ) - ), - wp_kses( $link, array( 'a' => array( 'href' => array() ) ) ) - ); - ?> -

-
-
-

- friendly_user_name() ) ); - echo ' '; - /* translators: %s: number of optimizable sizes and number of uploaded images */ - printf( esc_html__( 'With your current settings you can still optimize %1$s image sizes from your %2$s uploaded JPEG, PNG, and WebP images.', 'tiny-compress-images' ), '', '' ); - echo ' '; - printf( - wp_kses( - /* translators: %s: bulk optimization link */ - __( 'Start the %s to optimize the remainder of your library.', 'tiny-compress-images' ), - array( - 'a' => array( - 'href' => array(), +

+
+
+ +
+ + +
+ + +
+
+ + All images are optimized', 'tiny-compress-images' ), $strong ); + } else { + printf( + /* translators: %s: number of images that can still be optimized */ + wp_kses( _n( '%s image needs optimization', '%s images need optimization', $widget['images_remaining'], 'tiny-compress-images' ), $strong ), + esc_html( number_format_i18n( $widget['images_remaining'] ) ) + ); + } + ?> + +
+ + %s saved', 'tiny-compress-images' ), $strong ), + esc_html( size_format( $widget['bytes_saved'], 1 ) ) + ); + ?> + + + + %s credit remaining', '%s credits remaining', $widget['remaining_credits'], 'tiny-compress-images' ), $strong ), + esc_html( number_format_i18n( $widget['remaining_credits'] ) ) + ); + ?> + + +
+
+ + + + + + +
+ +
+ + +
+ + -
-
-

- saved %s of your media library size.', 'tiny-compress-images' ), - array( - 'span' => array(), - 'strong' => array(), - ) - ), - '%' - ); - ?> -

-
- - diff --git a/src/views/progress-circle.php b/src/views/progress-circle.php new file mode 100644 index 00000000..b62d5f56 --- /dev/null +++ b/src/views/progress-circle.php @@ -0,0 +1,75 @@ + + + +
+ +
+
%
+ +
+ +
+
diff --git a/test/integration/dashboardwidget.spec.ts b/test/integration/dashboardwidget.spec.ts index 50836014..8efe1d18 100644 --- a/test/integration/dashboardwidget.spec.ts +++ b/test/integration/dashboardwidget.spec.ts @@ -5,10 +5,23 @@ test.describe.configure({ mode: 'serial' }); let page: Page; +const widget = () => page.locator('#tinypng_dashboard_widget'); + +// The settings page fetches the account details over ajax after saving, +// so wait for that before the dashboard reads them. +async function setAccount(key: string) { + await setAPIKey(page, key); + await page.waitForURL(/settings-updated=true/); + await page.waitForLoadState('networkidle'); +} + test.describe('dashboardwidget', () => { test.beforeAll(async ({ browser }) => { - page = await browser.newPage(); + // The widget hides its notices when narrower than 560px, which it is + // in the two column dashboard at the default 1280px viewport. + page = await browser.newPage({ viewport: { width: 1440, height: 900 } }); await setAPIKey(page, ''); + await setCompressionTiming(page, 'auto'); await enableCompressionSizes(page, [], true); // enable all sizes }); @@ -17,46 +30,59 @@ test.describe('dashboardwidget', () => { }); test('show widget without images', async () => { + await setAPIKey(page, 'JPG123'); + await page.goto('/wp-admin/index.php'); - await expect(page.getByText('You do not seem to have uploaded any JPEG, PNG or WebP images yet.')).toBeVisible(); + + await expect(widget().getByText('George is hungry')).toBeVisible(); + await expect(widget().getByText('There are no images uploaded yet.')).toBeVisible(); }); - test('show widget without optimized images', async () => { - // It won't compress images without an API Key + test('show notice without api key', async () => { await setAPIKey(page, ''); - await setCompressionTiming(page, 'auto'); - - await uploadMedia(page, 'input-example.png'); await page.goto('/wp-admin/index.php'); - await expect( - page.getByText(`Hi Admin, you haven't compressed any images in your media library. If you like you can to optimize your whole library in one go with the bulk optimization page.`) - ).toBeVisible(); + + await expect(widget().getByText('Please register or provide an API key to start compressing images.')).toBeVisible(); }); - test('show widget with some images optimized', async () => { + test('show widget with images to optimize', async () => { + // It won't compress images without an API Key await setAPIKey(page, ''); + await uploadMedia(page, 'input-example.png'); - await setCompressionTiming(page, 'auto'); - - await uploadMedia(page, 'input-example.jpg'); + await page.goto('/wp-admin/index.php'); - await setAPIKey(page, 'JPG123'); + await expect(widget().getByText('1 image needs optimization')).toBeVisible(); + await expect(widget().getByText('Please register or provide an API key to start compressing images.')).toBeVisible(); + await expect(widget().getByRole('link', { name: 'Bulk Optimizer' })).toBeVisible(); + }); + test('show widget with all images optimized', async () => { + await setAPIKey(page, 'JPG123'); await uploadMedia(page, 'input-example.jpg'); await page.goto('/wp-admin/index.php'); - await expect(page.getByText('Admin, you are doing good. With your current settings you can still optimize')).toBeVisible(); + await expect(widget().getByText('All images are optimized')).toBeVisible(); + await expect(widget().getByText('100%')).toBeVisible(); + await expect(widget().getByRole('link', { name: 'Bulk Optimizer' })).not.toBeVisible(); }); - test('show widget with all images optimized', async () => { + test('show notice with low credits on free plan', async () => { + await setAccount('INSUFFICIENTCREDITS123'); + await page.goto('/wp-admin/index.php'); - await uploadMedia(page, 'input-example.jpg'); + await expect(widget().getByText('You are on a free plan with 1 compression left.')).toBeVisible(); + }); + + test('not show credits on paid plan', async () => { + await setAccount('JPG123'); await page.goto('/wp-admin/index.php'); - await expect(page.getByText('Admin, this is great! Your entire library is optimized!')).toBeVisible(); + await expect(widget().getByText('George is hungry')).toBeVisible(); + await expect(widget().getByText('compressions left')).not.toBeVisible(); }); }); diff --git a/test/unit/Tiny_Dashboard_Test.php b/test/unit/Tiny_Dashboard_Test.php new file mode 100644 index 00000000..a802b6fc --- /dev/null +++ b/test/unit/Tiny_Dashboard_Test.php @@ -0,0 +1,250 @@ +settings = $this->createMock(Tiny_Settings::class); + } + + /** + * helper to build optimization stats + * + * @param int $uploaded number of uploaded images + * @param int $remaining number of images remaining + * @param integer $unoptimized_size + * @param integer $optimized_size + * @return array optimization stats + */ + private function stats($uploaded, $remaining, $unoptimized_size = 0, $optimized_size = 0) + { + return array( + 'uploaded-images' => $uploaded, + 'optimized-image-sizes' => 0, + 'available-unoptimized-sizes' => 0, + 'optimized-library-size' => $optimized_size, + 'unoptimized-library-size' => $unoptimized_size, + 'estimated_credit_use' => 0, + 'available-for-optimization' => array_fill(0, $remaining, array('ID' => 1, 'post_title' => 'image')), + 'display-percentage' => 0, + ); + } + + public function test_is_empty_when_there_are_no_images() + { + $dashboard = new Tiny_Dashboard($this->settings); + $data = $dashboard->get_widget_data($this->stats(0, 0)); + + $this->assertSame('empty', $data['status']); + $this->assertSame(0, $data['percentage']); + $this->assertSame(0, $data['images_remaining']); + $this->assertSame('panda-waiting.png', $data['panda']); + } + + public function test_is_in_progress_when_images_remain() + { + $dashboard = new Tiny_Dashboard($this->settings); + $data = $dashboard->get_widget_data($this->stats(1250, 330)); + + $this->assertSame('in_progress', $data['status']); + $this->assertSame(920, $data['images_optimized']); + $this->assertSame(1250, $data['images_total']); + $this->assertSame(330, $data['images_remaining']); + $this->assertSame(73, $data['percentage']); + $this->assertSame('panda-waiting.png', $data['panda']); + } + + public function test_is_done_when_no_images_remain() + { + $dashboard = new Tiny_Dashboard($this->settings); + $data = $dashboard->get_widget_data($this->stats(1250, 0)); + + $this->assertSame('done', $data['status']); + $this->assertSame(1250, $data['images_optimized']); + $this->assertSame(100, $data['percentage']); + $this->assertSame('optimized', $data['label']); + $this->assertSame('panda-laying.png', $data['panda']); + } + + public function test_labels_up_to_half_as_getting_started() + { + $dashboard = new Tiny_Dashboard($this->settings); + + $one_percent = $dashboard->get_widget_data($this->stats(100, 99)); + $fifty_percent = $dashboard->get_widget_data($this->stats(100, 50)); + + $this->assertSame('getting started', $one_percent['label']); + $this->assertSame('getting started', $fifty_percent['label']); + } + + public function test_labels_above_half_as_halfway_there() + { + $dashboard = new Tiny_Dashboard($this->settings); + + $fifty_one_percent = $dashboard->get_widget_data($this->stats(100, 49)); + $seventy_five_percent = $dashboard->get_widget_data($this->stats(100, 25)); + + $this->assertSame('halfway there', $fifty_one_percent['label']); + $this->assertSame('halfway there', $seventy_five_percent['label']); + } + + public function test_labels_above_three_quarters_as_almost_there() + { + $dashboard = new Tiny_Dashboard($this->settings); + + $seventy_six_percent = $dashboard->get_widget_data($this->stats(100, 24)); + $ninety_nine_percent = $dashboard->get_widget_data($this->stats(1000, 1)); + + $this->assertSame('almost there', $seventy_six_percent['label']); + $this->assertSame('almost there', $ninety_nine_percent['label']); + } + + public function test_has_no_label_when_nothing_is_optimized() + { + $dashboard = new Tiny_Dashboard($this->settings); + $data = $dashboard->get_widget_data($this->stats(10, 10)); + + $this->assertSame('', $data['label']); + } + + public function test_does_not_show_100_percent_while_images_remain() + { + $dashboard = new Tiny_Dashboard($this->settings); + $data = $dashboard->get_widget_data($this->stats(1000, 1)); + + $this->assertSame('in_progress', $data['status']); + $this->assertSame(99, $data['percentage']); + } + + public function test_bytes_saved_is_the_library_size_reduction() + { + $dashboard = new Tiny_Dashboard($this->settings); + $data = $dashboard->get_widget_data($this->stats(10, 3, 5000, 3000)); + + $this->assertSame(2000, $data['bytes_saved']); + } + + public function test_bytes_saved_is_never_negative() + { + $dashboard = new Tiny_Dashboard($this->settings); + $data = $dashboard->get_widget_data($this->stats(10, 3, 3000, 5000)); + + $this->assertSame(0, $data['bytes_saved']); + } + + public function test_shows_remaining_credits_on_free_plan() + { + $this->settings->method('is_on_free_plan')->willReturn(true); + $this->settings->method('get_remaining_credits')->willReturn('210'); + $this->settings->method('has_api_key')->willReturn(true); + + $dashboard = new Tiny_Dashboard($this->settings); + $data = $dashboard->get_widget_data($this->stats(10, 1)); + + $this->assertSame(210, $data['remaining_credits']); + } + + public function test_hides_remaining_credits_on_paid_plan() + { + $this->settings->method('is_on_free_plan')->willReturn(false); + $this->settings->method('get_remaining_credits')->willReturn(210); + $this->settings->method('has_api_key')->willReturn(true); + + $dashboard = new Tiny_Dashboard($this->settings); + $data = $dashboard->get_widget_data($this->stats(10, 1)); + + $this->assertNull($data['remaining_credits']); + } + + public function test_hides_remaining_credits_when_unknown() + { + $this->settings->method('is_on_free_plan')->willReturn(true); + $this->settings->method('get_remaining_credits')->willReturn(false); + $this->settings->method('has_api_key')->willReturn(true); + + $dashboard = new Tiny_Dashboard($this->settings); + $data = $dashboard->get_widget_data($this->stats(10, 1)); + + $this->assertNull($data['remaining_credits']); + } + + public function test_has_no_notice_with_enough_credits() + { + $this->settings->method('is_on_free_plan')->willReturn(true); + $this->settings->method('get_remaining_credits')->willReturn(100); + $this->settings->method('has_api_key')->willReturn(true); + + $dashboard = new Tiny_Dashboard($this->settings); + $data = $dashboard->get_widget_data($this->stats(10, 1)); + + $this->assertNull($data['notice']); + } + + public function test_notifies_low_credits_below_100() + { + $this->settings->method('is_on_free_plan')->willReturn(true); + $this->settings->method('get_remaining_credits')->willReturn(99); + $this->settings->method('has_api_key')->willReturn(true); + + $dashboard = new Tiny_Dashboard($this->settings); + $data = $dashboard->get_widget_data($this->stats(10, 1)); + + $this->assertSame('low_credits', $data['notice']); + } + + public function test_notifies_low_credits_when_none_are_left() + { + $this->settings->method('is_on_free_plan')->willReturn(true); + $this->settings->method('get_remaining_credits')->willReturn(0); + $this->settings->method('has_api_key')->willReturn(true); + + $dashboard = new Tiny_Dashboard($this->settings); + $data = $dashboard->get_widget_data($this->stats(10, 1)); + + $this->assertSame('low_credits', $data['notice']); + } + + public function test_does_not_notify_low_credits_on_paid_plan() + { + $this->settings->method('is_on_free_plan')->willReturn(false); + $this->settings->method('get_remaining_credits')->willReturn(0); + $this->settings->method('has_api_key')->willReturn(true); + + $dashboard = new Tiny_Dashboard($this->settings); + $data = $dashboard->get_widget_data($this->stats(10, 1)); + + $this->assertNull($data['notice']); + } + + public function test_notifies_missing_api_key() + { + $this->settings->method('is_on_free_plan')->willReturn(false); + $this->settings->method('get_remaining_credits')->willReturn(false); + $this->settings->method('has_api_key')->willReturn(false); + + $dashboard = new Tiny_Dashboard($this->settings); + $data = $dashboard->get_widget_data($this->stats(10, 1)); + + $this->assertSame('no_api_key', $data['notice']); + } + + public function test_missing_api_key_takes_precedence_over_low_credits() + { + $this->settings->method('is_on_free_plan')->willReturn(true); + $this->settings->method('get_remaining_credits')->willReturn(10); + $this->settings->method('has_api_key')->willReturn(false); + + $dashboard = new Tiny_Dashboard($this->settings); + $data = $dashboard->get_widget_data($this->stats(10, 1)); + + $this->assertSame('no_api_key', $data['notice']); + } +} diff --git a/tiny-compress-images.php b/tiny-compress-images.php index 12a4879b..01282daa 100644 --- a/tiny-compress-images.php +++ b/tiny-compress-images.php @@ -27,6 +27,7 @@ require dirname( __FILE__ ) . '/src/class-tiny-cli.php'; require dirname( __FILE__ ) . '/src/class-tiny-conversion.php'; require dirname( __FILE__ ) . '/src/class-tiny-picture.php'; +require dirname( __FILE__ ) . '/src/class-tiny-dashboard.php'; require dirname( __FILE__ ) . '/src/class-tiny-apache-rewrite.php'; require dirname( __FILE__ ) . '/src/compatibility/wpml/class-tiny-wpml.php'; require dirname( __FILE__ ) . '/src/compatibility/as3cf/class-tiny-as3cf.php';