This commit is contained in:
2026-07-02 15:54:39 -06:00
commit 9883323161
17470 changed files with 4470592 additions and 0 deletions
@@ -0,0 +1,89 @@
<?php
namespace Vrts\Features;
use Vrts\Models\Test;
class Admin_Columns {
/**
* Constructor.
*/
public function __construct() {
add_action( 'admin_init', [ $this, 'init_admin_columns' ] );
}
/**
* Init admin columns.
*/
public function init_admin_columns() {
if ( current_user_can( 'manage_options' ) ) {
$custom_post_types = get_post_types([
'public' => true,
'_builtin' => false,
]);
$post_types = array_merge( [ 'post', 'page' ], $custom_post_types );
foreach ( $post_types as $post_type ) {
add_filter( 'manage_' . $post_type . '_posts_columns', [ $this, 'column_heading' ], 10, 1 );
add_action( 'manage_' . $post_type . '_posts_custom_column', [ $this, 'column_content' ], 10, 2 );
}
}
}
/**
* Add custom columns to the list table.
*
* @param array $columns Array of columns.
*
* @return array
*/
public function column_heading( $columns ) {
$added_columns = [];
$added_columns['vrts_testing_status'] = sprintf(
'<span class="vrts-status" title="%2$s %3$s">%1$s<span class="screen-reader-text">%2$s %3$s</span></span>',
vrts()->get_logo(),
vrts()->get_plugin_info( 'name' ),
__( 'Status', 'visual-regression-tests' )
);
return array_merge( $columns, $added_columns );
}
/**
* Display the content for the given column.
*
* @param string $column_name Column to display the content for.
* @param int $post_id Post to display the column content for.
*/
public function column_content( $column_name, $post_id ) {
switch ( $column_name ) {
case 'vrts_testing_status':
$test_id = Test::get_item_id( $post_id );
$item = (object) Test::get_item( $test_id );
if ( property_exists( $item, 'current_alert_id' ) ) {
$class = null === $item->current_alert_id ? 'vrts-icon-status--running' : 'vrts-icon-status--paused';
$text = null === $item->current_alert_id
? vrts()->get_plugin_info( 'name' ) . '&#13;' . esc_html__( 'Status: Running', 'visual-regression-tests' )
: vrts()->get_plugin_info( 'name' ) . '&#13;' . esc_html__( 'Status: Paused', 'visual-regression-tests' );
printf(
'<div aria-hidden="true" title="%s" class="vrts-icon-status %s"></div>
<span class="screen-reader-text">%s</span>',
esc_html( $text ),
esc_html( $class ),
esc_html( $text )
);
} else {
printf(
'<div aria-hidden="true" title="%s" class="vrts-icon-status"></div>
<span class="screen-reader-text">%s</span>',
esc_html( vrts()->get_plugin_info( 'name' ) . '&#13;' . esc_html__( 'Status: Testing not activated', 'visual-regression-tests' ) ),
esc_html( vrts()->get_plugin_info( 'name' ) . '&#13;' . esc_html__( 'Status: Testing not activated', 'visual-regression-tests' ) )
);
}//end if
return;
}//end switch
}
}
@@ -0,0 +1,56 @@
<?php
namespace Vrts\Features;
class Admin_Header {
/**
* Constructor.
*/
public function __construct() {
add_action('current_screen', function () {
$current_screen = get_current_screen();
if ( isset( $current_screen->id ) && strpos( $current_screen->id, 'vrts' ) !== false ) {
add_action( 'in_admin_header', [ $this, 'add_navigation' ] );
}
});
}
/**
* Add header navigation.
*/
public function add_navigation() {
global $submenu, $submenu_file, $plugin_page;
$menu_items = [];
$base_slug = 'vrts';
if ( isset( $submenu[ $base_slug ] ) ) {
foreach ( $submenu[ $base_slug ] as $sub_item ) {
if ( isset( $sub_item[2] ) && strpos( $sub_item[2], $base_slug ) !== false ) {
$url = admin_url( "admin.php?page={$sub_item[2]}" );
} else {
$url = admin_url( $sub_item[2] );
}
// Setup tab.
$menu_item = [
'text' => $sub_item[0],
'url' => isset( $url ) ? $url : $sub_item[2],
];
// Add state.
if ( $submenu_file === $sub_item[2] || $plugin_page === $sub_item[2] ) {
$menu_item['is_active'] = true;
}
$menu_items[] = $menu_item;
}
}//end if
vrts()->component( 'admin-header', [
'plugin_name' => vrts()->get_plugin_info( 'name' ),
'menu_items' => $menu_items,
]);
}
}
@@ -0,0 +1,75 @@
<?php
namespace Vrts\Features;
class Admin_Notices {
const OPTION_BASE_NAME = 'vrts_admin_notice_dismissed_';
/**
* Constructor.
*/
public function __construct() {
add_action( 'wp_ajax_vrts_admin_notice_dismiss', [ $this, 'wp_ajax_save_dismiss_status_ajax' ] );
}
/**
* Save admin notice dismiss status as option.
*/
public function wp_ajax_save_dismiss_status_ajax() {
$check_nonce = check_ajax_referer( 'vrts_admin_notice_nonce', 'security' );
$view = isset( $_POST['view'] ) ? sanitize_text_field( wp_unslash( $_POST['view'] ) ) : null;
if ( null !== $view && 1 === $check_nonce ) {
$option_id = self::OPTION_BASE_NAME . $view;
update_option( $option_id, true );
wp_die();
}
}
/**
* Get dismissed status of the notification from options.
*
* @param string $admin_notice_view_name The name of the view.
*/
private static function is_dismissed( $admin_notice_view_name ) {
return (bool) get_option( self::OPTION_BASE_NAME . $admin_notice_view_name, false );
}
/**
* Render the admin notification.
*
* @param string $admin_notice_view_name The name of the view.
* @param bool $is_dismissible Notification dismissible option.
* @param array $data Data to pass to the view.
*/
public static function render_notification( $admin_notice_view_name, $is_dismissible = false, $data = [] ) {
$data = array_merge([
'view' => $admin_notice_view_name,
], $data);
if ( $is_dismissible ) {
$is_dismissed = self::is_dismissed( $admin_notice_view_name );
if ( true !== $is_dismissed ) {
vrts()->component( 'admin-notification', $data );
}
} else {
vrts()->component( 'admin-notification', $data );
}
}
/**
* Remove all dismissed status of notifications from options.
*/
public static function delete_dismissed_options() {
global $wpdb;
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- It's OK.
$wpdb->query(
$wpdb->prepare(
"DELETE FROM `$wpdb->options` WHERE `option_name` Like %s",
$wpdb->esc_like( self::OPTION_BASE_NAME . '%' )
)
);
}
}
@@ -0,0 +1,48 @@
<?php
namespace Vrts\Features;
use Vrts\Core\Utilities\Url_Helpers;
use Vrts\Models\Alert;
class Admin {
/**
* Constructor.
*/
public function __construct() {
add_action( 'admin_menu', [ $this, 'add_main_menu' ] );
add_filter( 'plugin_action_links_' . plugin_basename( vrts()->get_plugin_file() ), [ $this, 'plugin_action_links' ] );
add_action( 'admin_init', 'Vrts\Features\Service::connect_service' );
}
/**
* Add main menu where other sub menus can be added to.
*/
public function add_main_menu() {
$count = Alert::get_total_items_grouped_by_test_run();
add_menu_page(
'VRTs',
$count ? 'VRTs <span class="update-plugins count-' . esc_attr( $count ) . '">' . esc_html( $count ) . '</span>' : 'VRTs',
'manage_options',
'vrts',
'',
//phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
'data:image/svg+xml;base64,' . base64_encode( vrts()->get_logo() ),
80
);
}
/**
* Show action links on the plugin screen.
*
* @param array $links Plugin Action links.
*
* @return array $links Plugin Action links.
*/
public function plugin_action_links( $links ) {
$links['tests'] = '<a href="' . esc_url( Url_Helpers::get_page_url( 'tests' ) ) . '" aria-label="' . esc_attr__( 'Tests', 'visual-regression-tests' ) . '">' . esc_html__( 'Tests', 'visual-regression-tests' ) . '</a>';
$links['settings'] = '<a href="' . esc_url( Url_Helpers::get_page_url( 'settings' ) ) . '" aria-label="' . esc_attr__( 'Settings', 'visual-regression-tests' ) . '">' . esc_html__( 'Settings', 'visual-regression-tests' ) . '</a>';
return $links;
}
}
@@ -0,0 +1,67 @@
<?php
namespace Vrts\Features;
use Vrts\Core\Utilities\Url_Helpers;
use Vrts\Services\Test_Service;
class Bulk_Actions {
/**
* Constructor.
*/
public function __construct() {
add_action( 'admin_init', function () {
$post_types = vrts()->get_public_post_types();
foreach ( $post_types as $post_type ) {
add_filter( 'bulk_actions-edit-' . $post_type, [ $this, 'register_bulk_test_option' ] );
add_filter( 'handle_bulk_actions-edit-' . $post_type, [ $this, 'handle_bulk_optimize_action' ], 10, 3 );
}
} );
}
/**
* Register bulk optimize option.
*
* @param array $bulk_actions Bulk actions.
* @return array
*/
public function register_bulk_test_option( $bulk_actions ) {
$bulk_actions['add-to-vrts'] = __( 'Add to VRTs', 'visual-regression-tests-service' );
return $bulk_actions;
}
/**
* Handle bulk optimize action.
*
* @param string $redirect_to Redirect to.
* @param string $doaction Action.
* @param array $post_ids Post ids.
* @return string
*/
public function handle_bulk_optimize_action( $redirect_to, $doaction, $post_ids ) {
if ( 'add-to-vrts' !== $doaction ) {
return $redirect_to;
}
$service = new Test_Service();
$created_tests = $service->create_tests( $post_ids );
$vrts_url = Url_Helpers::get_page_url( 'tests' );
if ( is_wp_error( $created_tests ) ) {
$redirect_to = add_query_arg([
'new-test-failed' => true,
], $vrts_url);
} else {
$redirect_to = add_query_arg([
'message' => 'success',
'new-tests-added' => true,
'post_ids' => wp_list_pluck( $created_tests, 'post_id' ),
], $vrts_url);
}
return $redirect_to;
}
}
@@ -0,0 +1,112 @@
<?php
namespace Vrts\Features;
use Vrts\Models\Test;
use Vrts\Models\Test_Run;
use Vrts\Services\Test_Run_Service;
use Vrts\Services\Test_Service;
class Cron_Jobs {
/**
* Max tries.
*
* @var int
*/
private $max_tries = 10;
/**
* Wait multiplicator.
*
* @var int
*/
private $wait_multiplicator = 2;
/**
* Initial wait.
*
* @var int
*/
private $initial_wait = 20;
/**
* Constructor.
*/
public function __construct() {
if ( ! wp_next_scheduled( 'vrts_fetch_updates_cron' ) ) {
wp_schedule_event( time(), 'hourly', 'vrts_fetch_updates_cron' );
}
add_action( 'vrts_fetch_updates_cron', [ $this, 'fetch_updates' ] );
add_action( 'vrts_fetch_test_updates', [ $this, 'fetch_test_updates' ], 10, 2 );
add_action( 'vrts_fetch_test_run_updates', [ $this, 'fetch_test_run_updates' ], 10, 2 );
}
/**
* Daily check connection status.
*/
public function fetch_updates() {
$service = new Test_Service();
$service->fetch_and_update_tests();
}
/**
* Remove jobs.
*/
public static function remove_jobs() {
wp_clear_scheduled_hook( 'vrts_connection_check_cron' );
wp_clear_scheduled_hook( 'vrts_fetch_updates_cron' );
}
/**
* Fetch test updates.
*
* @param int $test_id Test id.
* @param int $try_number Try number.
*/
public function fetch_test_updates( $test_id, $try_number = 1 ) {
$test = Test::get_item( $test_id );
if ( empty( $test ) || empty( $test->base_screenshot_date ) ) {
$service = new Test_Service();
$service->fetch_and_update_tests();
if ( $try_number < $this->max_tries ) {
$next_execution = time() + $this->initial_wait * $this->wait_multiplicator * $try_number;
wp_schedule_single_event( $next_execution, 'vrts_fetch_test_updates', [ $test_id, $try_number + 1 ] );
}
}
}
/**
* Fetch test run updates.
*
* @param int $test_run_id Test run id.
* @param int $try_number Try number.
*/
public function fetch_test_run_updates( $test_run_id, $try_number = 1 ) {
$test_run = Test_Run::get_item( $test_run_id );
if ( empty( $test_run ) || empty( $test_run->finished_at ) ) {
$service = new Test_Run_Service();
$service->fetch_and_update_test_runs();
if ( $try_number < $this->max_tries ) {
$next_execution = time() + $this->initial_wait * $this->wait_multiplicator * $try_number;
wp_schedule_single_event( $next_execution, 'vrts_fetch_test_run_updates', [ $test_run_id, $try_number + 1 ] );
}
}
}
/**
* Schedule initial fetch test updates.
*
* @param int $test_id Test id.
*/
public static function schedule_initial_fetch_test_updates( $test_id ) {
wp_schedule_single_event( time(), 'vrts_fetch_test_updates', [ $test_id, 1 ] );
}
/**
* Schedule initial fetch test run updates.
*
* @param int $test_run_id Test run id.
*/
public static function schedule_initial_fetch_test_run_updates( $test_run_id ) {
wp_schedule_single_event( time(), 'vrts_fetch_test_run_updates', [ $test_run_id, 1 ] );
}
}
@@ -0,0 +1,23 @@
<?php
namespace Vrts\Features;
use Vrts\Models\Test_Run;
class Deactivate {
/**
* Constructor.
*/
public function __construct() {
register_deactivation_hook( vrts()->get_plugin_file(), [ $this, 'deactivate' ] );
}
/**
* Deactivate plugin.
*/
public function deactivate() {
Test_Run::delete_all_not_finished();
Service::disconnect_service();
}
}
@@ -0,0 +1,130 @@
<?php
namespace Vrts\Features;
use Vrts\Core\Utilities\Date_Time_Helpers;
use Vrts\Core\Utilities\Image_Helpers;
use Vrts\Core\Utilities\Url_Helpers;
use Vrts\Models\Test;
use Vrts\Features\Subscription;
class Enqueue_Scripts {
/**
* Constructor.
*/
public function __construct() {
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] );
add_action( 'enqueue_block_editor_assets', [ $this, 'enqueue_block_editor_assets' ] );
}
/**
* Register and Enqueue CSS and JS.
*/
public function enqueue_scripts() {
if ( current_user_can( 'manage_options' ) ) {
$admin_assets_path = vrts()->get_plugin_path( 'build/admin.asset.php' );
if ( ! file_exists( $admin_assets_path ) ) {
// add admin notice.
// You need to run `npm start` or `npm run build`.'.
return;
}
$admin_assets_data = include $admin_assets_path;
// Register CSS.
wp_register_style( 'vrts-admin', vrts()->get_plugin_url( 'build/admin.css' ), [], $admin_assets_data['version'] );
// Register JS.
wp_register_script( 'vrts-admin', vrts()->get_plugin_url( 'build/admin.js' ), $admin_assets_data['dependencies'], $admin_assets_data['version'], true );
// Enqueue CSS.
wp_enqueue_style( 'vrts-admin' );
// Enqueue JS.
wp_enqueue_script( 'vrts-admin' );
// Localize scripts.
wp_localize_script(
'vrts-admin',
'vrts_admin_vars',
[
'rest_url' => esc_url_raw( rest_url( 'vrts/v1' ) ),
'rest_nonce' => wp_create_nonce( 'wp_rest' ),
'pluginUrl' => vrts()->get_plugin_url(),
'currentUserId' => get_current_user_id(),
'onboarding' => apply_filters( 'vrts_onboarding', null ),
]
);
}//end if
}
/**
* Register and Enqueue Editor CSS and JS.
*/
public function enqueue_block_editor_assets() {
if ( current_user_can( 'manage_options' ) ) {
global $post;
if ( ! $post ) {
return;
}
$custom_post_types = get_post_types([
'public' => true,
'_builtin' => false,
]);
$post_types = array_merge( [ 'post', 'page' ], $custom_post_types );
if ( ! in_array( $post->post_type, $post_types, true ) ) {
return;
}
$editor_assets_path = vrts()->get_plugin_path( 'build/editor.asset.php' );
if ( ! file_exists( $editor_assets_path ) ) {
// add editor notice.
// You need to run `npm start` or `npm run build`.'.
return;
}
$editor_assets_data = include $editor_assets_path;
// Register CSS.
wp_register_style( 'vrts-editor', vrts()->get_plugin_url( 'build/editor.css' ), [], $editor_assets_data['version'] );
// Register JS.
wp_register_script( 'vrts-editor', vrts()->get_plugin_url( 'build/editor.js' ), $editor_assets_data['dependencies'], $editor_assets_data['version'], true );
// Enqueue CSS.
wp_enqueue_style( 'vrts-editor' );
// Enqueue JS.
wp_enqueue_script( 'vrts-editor' );
// Localize scripts.
$test_id = Test::get_item_id( $post->ID );
$test = (object) Test::get_item( $test_id );
wp_localize_script(
'vrts-editor',
'vrts_editor_vars',
[
'plugin_name' => vrts()->get_plugin_info( 'name' ),
'rest_url' => esc_url_raw( rest_url() ),
'has_post_alert' => isset( $test->current_alert_id ) ? ! is_null( $test->current_alert_id ) : false,
'base_screenshot_url' => Image_Helpers::get_screenshot_url( $test, 'base' ),
'base_screenshot_date' => Date_Time_Helpers::get_formatted_date_time( $test->base_screenshot_date ?? null ),
'remaining_tests' => Subscription::get_remaining_tests(),
'total_tests' => Subscription::get_total_tests(),
'upgrade_url' => Url_Helpers::get_page_url( 'upgrade' ),
'plugin_url' => Url_Helpers::get_page_url( 'tests' ),
'is_connected' => Service::is_connected(),
'test_status' => Test::get_status_data( $test ),
'screenshot' => Test::get_screenshot_data( $test ),
'test_settings' => [
'test_id' => isset( $test->id ) ? $test->id : null,
'hide_css_selectors' => isset( $test->hide_css_selectors ) ? $test->hide_css_selectors : null,
],
]
);
}//end if
}
}
@@ -0,0 +1,111 @@
<?php
namespace Vrts\Features;
use Vrts\Features\Service;
use Vrts\Tables\Alerts_Table;
use Vrts\Tables\Tests_Table;
use Vrts\Tables\Test_Runs_Table;
class Install {
const ACTIVATION_TRANSIENT = 'vrts_activation';
/**
* Constructor.
*/
public function __construct() {
register_activation_hook( vrts()->get_plugin_file(), [ $this, 'install' ] );
register_activation_hook( vrts()->get_plugin_file(), [ $this, 'set_activation_admin_notice_transient' ] );
add_action( 'admin_notices', [ $this, 'activation_admin_notice' ] );
add_action( 'init', [ $this, 'install' ], 10, 2 );
add_action( 'upgrader_process_complete', [ $this, 'on_upgrade' ], 10, 2 );
add_action( 'vrts_plugin_on_upgrade', [ $this, 'install' ], 10, 2 );
}
/**
* Install plugin.
*
* @param bool $network_wide If the plugin has been activated network wide.
*/
public function install( $network_wide ) {
if ( is_multisite() && $network_wide ) {
global $wpdb;
// Direct DB query and no caching are OK to use in this case.
// @codingStandardsIgnoreStart.
$blog_ids = $wpdb->get_col("SELECT blog_id FROM $wpdb->blogs");
// @codingStandardsIgnoreEnd.
foreach ( $blog_ids as $blog_id ) {
switch_to_blog( $blog_id );
// call here function to install tables, options etc.
$this->install_tables();
$this->connect_service();
restore_current_blog();
}
} else {
// call here function to install tables, options etc.
$this->install_tables();
$this->connect_service();
}
}
/**
* Install plugin tables.
*/
private function install_tables() {
Alerts_Table::install_table();
Tests_Table::install_table();
Test_Runs_Table::install_table();
}
/**
* Install plugin tables.
*/
private function connect_service() {
Service::connect_service();
}
/**
* Set activation transient.
*/
public function set_activation_admin_notice_transient() {
set_transient( self::ACTIVATION_TRANSIENT, true, 5 );
}
/**
* Display activation admin notice.
*/
public function activation_admin_notice() {
if ( get_transient( self::ACTIVATION_TRANSIENT ) ) {
Admin_Notices::render_notification( 'plugin_activated' );
delete_transient( self::ACTIVATION_TRANSIENT );
}
}
/**
* On upgrade.
*
* @param object $upgrader WP_Upgrader instance.
* @param array $options Array of bulk item update data.
*/
public function on_upgrade( $upgrader, $options ) {
if ( 'update' !== $options['action'] ) {
return;
}
if ( 'core' === $options['type'] ) {
return;
}
if ( 'plugin' === $options['type'] && isset( $options['plugins'] ) ) {
foreach ( $options['plugins'] as $plugin ) {
if ( plugin_basename( vrts()->get_plugin_file() ) === $plugin ) {
do_action( 'vrts_plugin_on_upgrade' );
}
}
}
}
}
@@ -0,0 +1,238 @@
<?php
namespace Vrts\Features;
use Vrts\Core\Utilities\Date_Time_Helpers;
use Vrts\Core\Utilities\Image_Helpers;
use Vrts\Core\Utilities\Url_Helpers;
use Vrts\Models\Test;
use Vrts\Services\Test_Service;
class Metaboxes {
/**
* Field key for the "Run Tests" checkbox post meta.
* _ (underscore prefix) represents a protected meta key.
*
* @var string
*/
public static $field_test_status_key = '_vrts_testing_status';
/**
* Field key for the "is new test" post meta.
* _ (underscore prefix) represents a protected meta key.
*
* @var string
*/
public static $field_is_new_test_key = '_vrts_is_new_test';
/**
* Nonce.
*
* @var string
*/
protected $nonce = 'vrts_metabox_nonce';
/**
* Constructor.
*/
public function __construct() {
add_action( 'add_meta_boxes', [ $this, 'add_meta_boxes' ] );
add_action( 'rest_api_init', [ $this, 'add_rest_actions' ] );
add_action( 'save_post', [ $this, 'save_meta_boxes_data' ], 10, 2 );
}
/**
* Get the value of the static key.
*/
public static function get_post_meta_key_status() {
return self::$field_test_status_key;
}
/**
* Get the value of the static key.
*/
public static function get_post_meta_key_is_new_test() {
return self::$field_is_new_test_key;
}
/**
* Is new test.
*
* @param int $post_id WP Post id.
*/
public static function is_new_test( $post_id ) {
$test = Test::get_item_by_post_id( $post_id );
return empty( $test ) ? false : ! $test->service_test_id;
}
/**
* Add meta boxes.
*/
public function add_meta_boxes() {
if ( current_user_can( 'manage_options' ) ) {
$custom_post_types = get_post_types([
'public' => true,
'_builtin' => false,
]);
add_meta_box(
'vrts_post_options_metabox',
vrts()->get_plugin_info( 'name' ),
[ $this, 'render_metabox' ],
array_merge( [ 'post', 'page' ], $custom_post_types ),
'side',
'default',
[ '__back_compat_meta_box' => true ]
);
}
}
/**
* Add rest actions.
*/
public function add_rest_actions() {
if ( current_user_can( 'manage_options' ) ) {
$custom_post_types = get_post_types([
'public' => true,
'_builtin' => false,
]);
foreach ( array_merge( [ 'post', 'page' ], $custom_post_types ) as $custom_post_type ) {
add_action( 'rest_after_insert_' . $custom_post_type, [ $this, 'update_rest_data' ], 10, 2 );
}
}
}
/**
* Render meta box.
*/
public function render_metabox() {
global $post;
$post_id = $post->ID ? $post->ID : 0;
$test_id = Test::get_item_id( $post_id );
$test = (object) Test::get_item( $test_id );
$run_tests_checked = ! is_null( $test_id );
$alert_id = $test->current_alert_id ?? null;
$testing_status_instructions = '';
if ( $alert_id ) {
$alert_link = Url_Helpers::get_alert_page( $alert_id );
$testing_status_instructions .= sprintf(
/* translators: %1$s and %2$s: link wrapper. */
esc_html__( '%1$sView Alert%2$s', 'visual-regression-tests' ),
'<a href="' . esc_url( $alert_link ) . '">',
'</a>'
);
}
vrts()->component('metabox-classic-editor', [
'post_id' => $post_id,
'nonce' => $this->nonce,
'plugin_url' => Url_Helpers::get_page_url( 'tests' ),
'run_tests_checked' => $run_tests_checked,
'field_test_status_key' => self::$field_test_status_key,
'has_post_alert' => isset( $test->current_alert_id ) ? ! is_null( $test->current_alert_id ) : false,
'base_screenshot_url' => Image_Helpers::get_screenshot_url( $test, 'base' ),
'base_screenshot_date' => Date_Time_Helpers::get_formatted_date_time( $test->base_screenshot_date ?? null ),
'testing_status_instructions' => $testing_status_instructions,
'is_new_test' => self::is_new_test( $post_id ),
'remaining_tests' => Subscription::get_remaining_tests(),
'total_tests' => Subscription::get_total_tests(),
'is_connected' => Service::is_connected(),
'test_status' => Test::get_status_data( $test ),
'screenshot' => Test::get_screenshot_data( $test ),
'test_settings' => [
'test_id' => isset( $test->id ) ? $test->id : null,
'hide_css_selectors' => isset( $test->hide_css_selectors ) ? $test->hide_css_selectors : null,
],
]);
}
/**
* Add meta boxes.
*
* @param int $post_id Post ID.
* @param WP_Post $post Post object.
*/
public function save_meta_boxes_data( $post_id, $post ) {
$edit_cap = get_post_type_object( $post->post_type )->cap->edit_post;
if ( ! current_user_can( $edit_cap, $post_id ) ) {
return;
}
$nonce = sanitize_text_field( wp_unslash( $_POST[ $this->nonce ] ?? '' ) );
// Verify nonce. Only valid when using classic editor.
if ( ! wp_verify_nonce( $nonce, $this->nonce ) ) {
return;
}
// Save "Run Tests" checkbox value to post meta.
if ( array_key_exists( self::$field_test_status_key, $_POST ) && '1' === $_POST[ self::$field_test_status_key ] ) {
$service = new Test_Service();
$service->create_test( $post_id );
} else {
// Delete data from tests database table if "Run Tests" checkbox is not checked.
$test_id = Test::get_item_id( $post_id );
if ( $test_id ) {
$service = new Test_Service();
$service->delete_test( (int) $test_id );
}
}
// Save Settings of the test.
$test = (object) Test::get_item_by_post_id( $post_id );
$test_id = isset( $_POST['test_id'] ) ? (int) sanitize_text_field( wp_unslash( $_POST['test_id'] ) ) : 0;
if ( ! empty( $test ) && ! empty( $test->id ) && (int) $test->id === (int) $test_id ) {
$hide_css_selectors = isset( $_POST['hide_css_selectors'] ) ? sanitize_text_field( wp_unslash( $_POST['hide_css_selectors'] ) ) : '';
$test_service = new Test_Service();
$test_service->update_css_hide_selectors( $test_id, $hide_css_selectors );
}
}
/**
* Delete post meta keys.
*/
public static function delete_meta_keys() {
global $wpdb;
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- It's ok.
$wpdb->delete(
$wpdb->postmeta,
[
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- TODO: Check later
'meta_key' => self::get_post_meta_key_status(),
]
);
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- It's ok.
$wpdb->delete(
$wpdb->postmeta,
[
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- TODO: Check later
'meta_key' => self::get_post_meta_key_is_new_test(),
]
);
}
/**
* Update rest data.
*
* @param WP_Post $post Post object.
* @param WP_REST_Request $request Request object.
*/
public function update_rest_data( $post, $request ) {
$vrts_params = $request->get_param( 'vrts' );
if ( array_key_exists( 'hide_css_selectors', $vrts_params ?? [] ) ) {
$hide_css_selectors = $vrts_params['hide_css_selectors'] ? sanitize_text_field( $vrts_params['hide_css_selectors'] ) : '';
$test_id = Test::get_item_id( $post->ID );
$test_service = new Test_Service();
$test_service->update_css_hide_selectors( $test_id, $hide_css_selectors );
}
}
}
@@ -0,0 +1,232 @@
<?php
namespace Vrts\Features;
use Vrts\Models\Alert;
use Vrts\Models\Test;
class Onboarding {
/**
* Constructor.
*/
public function __construct() {
add_action( 'rest_api_init', [ $this, 'register_rest_field' ] );
add_filter( 'vrts_onboarding', [ $this, 'get_onboarding' ] );
}
/**
* Add user meta setting.
*/
public function register_rest_field() {
register_rest_field( 'user', 'vrts_onboarding', [
'get_callback' => function ( $user ) {
return get_user_meta( $user['id'], 'vrts_onboarding', true ) ?: [];
},
'update_callback' => function ( $value, $user ) {
return update_user_meta( $user->ID, 'vrts_onboarding', $value );
},
'schema' => [
'type' => 'object',
'properties' => [
'completed' => [
'type' => 'array',
'default' => [],
],
],
],
] );
}
/**
* Get onboarding.
*
* @return array
*/
public function get_onboarding() {
$onboardins = $this->get_onboardings();
foreach ( $onboardins as $onboarding ) {
if ( $this->has_user_completed_onboarding( $onboarding['id'] ) ) {
continue;
}
if ( call_user_func( $onboarding['permission_callback'] ) ) {
unset( $onboarding['permission_callback'] );
return $onboarding;
}
}
return false;
}
/**
* Has user completed onboarding.
*
* @param string $onboarding_id the onboarding id.
* @return bool
*/
public function has_user_completed_onboarding( $onboarding_id ) {
$onboarding = get_user_meta( get_current_user_id(), 'vrts_onboarding', true ) ?: [];
return in_array( $onboarding_id, $onboarding['completed'] ?? [], true );
}
/**
* Get onboardings.
*
* @return array
*/
public function get_onboardings() {
return [
[
'id' => 'tests-welcome',
'permission_callback' => [ $this, 'should_display_tests_welcome_onboarding' ],
'steps' => [
[
'title' => wp_kses_post( __( '👋 Howdy, welcome aboard!', 'visual-regression-tests' ) ),
'description' => wp_kses_post( __( "With our VRTs plugin, you can effortlessly maintain your website's visual consistency. <br><br><strong>Automatically detect visual changes</strong> and <strong>receive Alerts</strong> to achieve pixel-perfect precision.", 'visual-regression-tests' ) ),
],
[
'title' => wp_kses_post( __( '⏰ Daily Tests & Pro Automations', 'visual-regression-tests' ) ),
'description' => wp_kses_post( __( 'The <strong>Daily Test Run</strong> captures screenshots of your <strong>Test pages</strong> and performs <strong>side-by-side comparisons</strong> to instantly spot changes.<br><br><strong>Upgrade to Pro</strong> to automate Tests for WordPress and plugin updates, integrate deployment pipelines via API, and run Manual Tests on demand.', 'visual-regression-tests' ) ),
],
[
'side' => 'right',
'align' => 'start',
'padding' => 8,
'element' => '#show-modal-add-new',
'title' => wp_kses_post( __( "🚀 Let's get started!", 'visual-regression-tests' ) ),
'description' => wp_kses_post( __( 'Add your first Test here to enable VRTs for the selected page.', 'visual-regression-tests' ) ),
],
],
],
[
'id' => 'first-test',
'permission_callback' => [ $this, 'should_display_first_test_onboarding' ],
'steps' => [
[
'side' => 'bottom',
'align' => 'center',
'element' => '.wp-list-table tbody tr:first-child',
'title' => wp_kses_post( __( '🥳 Yay, you created your first VRT!', 'visual-regression-tests' ) ),
'description' => wp_kses_post( __( 'Starting from tomorrow, your Test will <strong>run daily</strong>, ensuring consistent monitoring of your page.', 'visual-regression-tests' ) ),
],
[
'element' => '.vrts-admin-header__navigation-link[href$="admin.php?page=vrts-settings"]',
'title' => wp_kses_post( __( '🛠️ Fine-tune your setup', 'visual-regression-tests' ) ),
'description' => wp_kses_post( __( 'Further customize your Test configuration and plugin settings for an optimized experience.', 'visual-regression-tests' ) ),
],
],
],
[
'id' => 'run-test',
'permission_callback' => [ $this, 'should_display_run_test_onboarding' ],
'steps' => [
[
'padding' => 2,
'element' => '.wp-list-table .vrts-run-test',
'title' => wp_kses_post( __( '🔬 Run your Test now', 'visual-regression-tests' ) ),
'description' => wp_kses_post( __( 'Want to see your Test in action? <br>Give it a go and run this Test now!', 'visual-regression-tests' ) ),
],
],
],
[
'id' => 'runs-introduction',
'permission_callback' => [ $this, 'should_display_runs_introduction_onboarding' ],
'steps' => [
[
'side' => 'bottom',
'align' => 'center',
'element' => '.vrts-admin-header__navigation-link[href$="admin.php?page=vrts-runs"]',
'title' => wp_kses_post( __( '🚀 Meet the new Runs!', 'visual-regression-tests' ) ),
'description' => wp_kses_post( __( 'Alerts are now bundled into Runs. Get a single report for each daily test, manual test, API trigger, or new: <strong>WordPress & plugin update (Pro)</strong>!', 'visual-regression-tests' ) ),
],
],
],
];
}
/**
* Should display tests welcome onboarding.
*
* @return bool
*/
public function should_display_tests_welcome_onboarding() {
$page = sanitize_text_field( wp_unslash( $_GET['page'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verification is not required here.
if ( 'vrts' === $page ) {
$frontpage_id = get_option( 'page_on_front' );
$is_front_page_added = ! is_null( Test::get_item_id( $frontpage_id ) );
$next_id = Test::get_autoincrement_value();
if ( 1 === $next_id || ( $is_front_page_added && 2 === $next_id ) ) {
return true;
}
}
return false;
}
/**
* Should display first test onboarding.
*
* @return bool
*/
public function should_display_first_test_onboarding() {
$page = sanitize_text_field( wp_unslash( $_GET['page'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verification is not required here.
$is_new_test_added = isset( $_GET['new-test-added'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verification is not required here.
if ( 'vrts' === $page && $is_new_test_added ) {
$frontpage_id = get_option( 'page_on_front' );
$is_front_page_added = ! is_null( Test::get_item_id( $frontpage_id ) );
$next_id = Test::get_autoincrement_value();
if ( 2 === $next_id || ( $is_front_page_added && 3 === $next_id ) ) {
return true;
}
}
return false;
}
/**
* Should display run test onboarding.
*
* @return bool
*/
public function should_display_run_test_onboarding() {
$page = sanitize_text_field( wp_unslash( $_GET['page'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verification is not required here.
$has_subscription = (bool) Subscription::get_subscription_status();
if ( 'vrts' === $page && $has_subscription ) {
$tests = Test::get_items();
foreach ( $tests as $test ) {
$status = Test::get_calculated_status( $test );
if ( 'scheduled' === $status ) {
return true;
}
}
}
return false;
}
/**
* Should display run introduction onboarding.
*
* @return bool
*/
public function should_display_runs_introduction_onboarding() {
$page = sanitize_text_field( wp_unslash( $_GET['page'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verification is not required here.
if ( in_array( $page, [ 'vrts', 'vrts-runs', 'vrts-settings' ], true ) ) {
$has_migrated_alerts = get_option( 'vrts_test_runs_has_migrated_alerts' );
if ( $has_migrated_alerts ) {
return true;
}
}
return false;
}
}
@@ -0,0 +1,103 @@
<?php
namespace Vrts\Features;
use Vrts\Models\Test;
use Vrts\Models\Alert;
use Vrts\Services\Test_Service;
class Post_Update_Actions {
/**
* Constructor.
*/
public function __construct() {
add_action( 'wp_after_insert_post', [ $this, 'resume_test' ] );
add_action( 'trashed_post', [ $this, 'on_trash_post_action' ], 10, 2 );
add_action( 'transition_post_status', [ $this, 'on_transition_post_status_action' ], 10, 3 );
add_action( 'update_option_vrts_remaining_tests', [ $this, 'on_update_option_vrts_remaining_tests_action' ], 10, 2 );
add_action( 'post_updated', [ $this, 'on_post_updated_action' ], 10, 3 );
}
/**
* Add meta boxes.
*
* @param int $post_id Post ID.
*/
public function resume_test( $post_id ) {
// If post has test, update the screenshot to the latest version.
if ( Test::get_item_id( $post_id ) ) {
$service = new Test_Service();
$service->resume_test( $post_id );
}
}
/**
* Delete tests when post is trashed.
*
* @param int $post_id Post ID.
*/
public function on_trash_post_action( $post_id ) {
// If trashed post has test, delete the test too.
$test_id = Test::get_item_id( $post_id );
if ( $test_id ) {
Test::delete( $test_id );
// If an alert exists already, archive it too.
Alert::set_alert_state_for_post_id( $post_id, 1 );
}
}
/**
* Create or delete test remotely when post is published or unpublished.
*
* @param string $new_status New post status.
* @param string $old_status Old post status.
* @param WP_Post $post Post object.
*/
public function on_transition_post_status_action( $new_status, $old_status, $post ) {
// If post has test and no active alerts, update the screenshot to the latest version.
$test = Test::get_item_by_post_id( $post->ID );
if ( $test ) {
if ( 'publish' === $new_status && 'publish' !== $old_status ) {
$service = new Test_Service();
$service->create_remote_test( $post, (array) $test );
}
if ( 'publish' === $old_status && 'publish' !== $new_status ) {
$service = new Test_Service();
$service->delete_remote_test( $test );
}
}
}
/**
* Resume tests when remaining tests option is updated.
*
* @param string $old_value Old option value.
* @param string $value New option value.
*/
public function on_update_option_vrts_remaining_tests_action( $old_value, $value ) {
if ( intval( $old_value ) === 0 && intval( $value ) > 0 ) {
$service = new Test_Service();
$service->resume_stale_tests();
}
}
/**
* Update test URL when post slug is updated.
*
* @param int $post_id Post ID.
* @param WP_Post $post_after Post object after update.
* @param WP_Post $post_before Post object before update.
*/
public function on_post_updated_action( $post_id, $post_after, $post_before ) {
$test = Test::get_item_by_post_id( $post_id );
if ( $test ) {
if ( $post_after->post_name !== $post_before->post_name ) {
$service = new Service();
$service->update_test( $test->service_test_id, [
'url' => get_permalink( $post_id ),
] );
}
}
}
}
@@ -0,0 +1,395 @@
<?php
namespace Vrts\Features;
use Vrts\Models\Alert;
use Vrts\Models\Test;
use Vrts\Services\Test_Service;
class Service {
const DB_VERSION = '1.2';
const SERVICE = 'vrts_service';
const BASE_URL = VRTS_SERVICE_ENDPOINT;
/**
* Connect current website to VRTs Service.
*/
public static function connect_service() {
$option_name = self::SERVICE . '_version';
$installed_version = get_option( $option_name );
if ( self::DB_VERSION !== $installed_version ) {
update_option( $option_name, self::DB_VERSION );
}//end if
if ( ! self::is_connected() ) {
self::create_site();
}
if ( self::is_connected() && ! self::has_secret() ) {
self::create_secret();
}
if ( $installed_version && version_compare( $installed_version, '1.2', '<' ) ) {
$service_project_id = get_option( 'vrts_project_id' );
$service_api_route = 'sites/' . $service_project_id;
self::rest_service_request( $service_api_route, [], 'put' );
}
}
/**
* Rerty connection.
*/
public static function retry_connection() {
return static::create_site( true );
}
/**
* Helper to create site on service.
*
* @return bool
*/
private static function create_site() {
if ( static::is_connected() ) {
return;
}
$time = current_time( 'mysql' );
$rest_base_url = self::get_rest_url();
$service_api_route = 'sites';
$create_token = md5( 'verysecret' . $time );
$parameters = [
'create_token' => $create_token,
'rest_url' => $rest_base_url,
'admin_ajax_url' => admin_url( 'admin-ajax.php' ),
'requested_at' => $time,
];
if ( ! empty( get_option( 'vrts_project_id' ) ) && ! empty( get_option( 'vrts_project_token' ) ) ) {
$parameters['project_id'] = get_option( 'vrts_project_id' );
$parameters['project_token'] = get_option( 'vrts_project_token' );
$parameters['project_secret'] = get_option( 'vrts_project_secret' );
$parameters['tests'] = Test::get_all_service_test_ids();
}
$service_request = self::rest_service_request( $service_api_route, $parameters, 'post' );
delete_option( 'vrts_disconnected' );
if ( 201 === $service_request['status_code'] || 200 === $service_request['status_code'] ) {
$data = $service_request['response'];
update_option( 'vrts_project_id', $data['id'] );
update_option( 'vrts_project_token', $data['token'] );
update_option( 'vrts_project_secret', $data['secret'] ?? null );
Subscription::update_available_tests( $data['remaining_credits'], $data['total_credits'], $data['has_subscription'], $data['tier_id'] );
self::add_homepage_test();
return true;
} else {
update_option( 'vrts_disconnected', 1 );
}
return false;
}
/**
* Connect current website to VRTs Service.
*
* @param string $service_api_route the service api route.
* @param array $parameters the parameters.
* @param string $request_type the request type.
*/
public static function rest_service_request( $service_api_route, $parameters = [], $request_type = '' ) {
$request_url = self::BASE_URL . $service_api_route;
$service_project_id = get_option( 'vrts_project_id' );
$service_project_token = get_option( 'vrts_project_token' );
$response = [];
$args = [
'project_id' => $service_project_id,
'headers' => [
'Content-Type' => 'application/json; charset=utf-8',
'Authorization' => 'Bearer ' . $service_project_token,
],
'body' => wp_json_encode( $parameters ),
'data_format' => 'body',
];
// If project already created, attach project id and service token.
if ( $service_project_id && $service_project_token ) {
$args['project_id'] = $service_project_id;
$args['headers']['Authorization'] = 'Bearer ' . $service_project_token;
}
add_filter( 'http_headers_useragent', [ static::class, 'set_user_agent' ], 10 );
switch ( $request_type ) {
case 'get':
$args = [
'method' => 'GET',
'project_id' => $service_project_id,
'headers' => [
'Authorization' => 'Bearer ' . $service_project_token,
],
'body' => $parameters,
'data_format' => 'body',
];
$data = wp_remote_post( $request_url, $args );
$response = [
'response' => json_decode( wp_remote_retrieve_body( $data ), true ),
'status_code' => wp_remote_retrieve_response_code( $data ),
];
break;
case 'delete':
$args['method'] = 'DELETE';
$data = wp_remote_post( $request_url, $args );
break;
case 'put':
$args['method'] = 'PUT';
$data = wp_remote_post( $request_url, $args );
break;
default:
$data = wp_remote_post( $request_url, $args );
$response = [
'response' => json_decode( wp_remote_retrieve_body( $data ), true ),
'status_code' => wp_remote_retrieve_response_code( $data ),
];
break;
}//end switch
if ( empty( $response ) ) {
$response = [
'response' => $data,
'status_code' => wp_remote_retrieve_response_code( $data ),
];
}
remove_filter( 'http_headers_useragent', [ static::class, 'set_user_agent' ], 10 );
return $response;
}
/**
* Send request to server to resume test.
*
* @param int $post_id the post id.
*/
public static function resume_test( $post_id ) {
$service_test_id = Test::get_service_test_id_by_post_id( $post_id );
if ( $service_test_id ) {
$service_api_route = 'tests/' . $service_test_id . '/resume';
$response = self::rest_service_request( $service_api_route, [], 'post' );
}
}
/**
* Send request to server to delete test.
*
* @param int $service_test_id the service test id.
*/
public static function delete_test( $service_test_id ) {
$service_api_route = 'tests/' . $service_test_id;
$response = self::rest_service_request( $service_api_route, [], 'delete' );
return 200 === $response['status_code'];
}
/**
* Send request to server to update test.
*
* @param int $service_test_id the service test id.
* @param array $data the data.
*/
public static function update_test( $service_test_id, $data ) {
$service_api_route = 'tests/' . $service_test_id;
$response = self::rest_service_request( $service_api_route, $data, 'put' );
return 200 === $response['status_code'];
}
/**
* Send request to server to resume tests.
*/
public static function resume_tests() {
$service_project_id = get_option( 'vrts_project_id' );
$service_api_route = 'sites/' . $service_project_id;
self::rest_service_request( $service_api_route . '/resume', [], 'post' );
}
/**
* Add homepage as a default test.
*/
public static function add_homepage_test() {
$option_name = 'vrts_homepage_added';
$homepage_added = get_option( $option_name );
// If plugin was previously activated, dont add homepage again.
if ( ! $homepage_added ) {
$homepage_id = get_option( 'page_on_front' );
// Save data to custom database table.
$test_service = new Test_Service();
$test_service->create_test( $homepage_id );
update_option( $option_name, 1 );
}
}
/**
* Get rest url for default language if WPML is installed.
*/
private static function get_rest_url() {
// Exlusion for WPML installations.
global $sitepress;
$rest_url = rest_url( 'vrts/v1/service' );
if ( $sitepress ) {
// WPML Get languages.
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- It's ok.
$wpml_current_lang = apply_filters( 'wpml_current_language', null );
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- It's ok.
$wpml_default_lang = apply_filters( 'wpml_default_language', null );
// If current language is not default, switch to default language to get the rest url.
if ( $wpml_current_lang !== $wpml_default_lang ) {
$sitepress->switch_lang( $wpml_default_lang );
$rest_url = rest_url( 'vrts/v1/service' );
// Switch back to the current language.
$sitepress->switch_lang( $wpml_current_lang );
}
}
return $rest_url;
}
/**
* Run manual tests.
*
* @param string[] $service_test_ids the service test ids.
* @param array $options the options.
*/
public static function run_manual_tests( $service_test_ids, $options = [] ) {
$service_project_id = get_option( 'vrts_project_id' );
$service_api_route = 'sites/' . $service_project_id . '/trigger';
return self::rest_service_request( $service_api_route, array_merge( $options, [
'ids' => $service_test_ids,
]), 'post' );
}
/**
* Mark alert as false positive.
*
* @param int $alert_id Alert id.
*/
public static function mark_alert_as_false_positive( $alert_id ) {
$alert = Alert::get_item( $alert_id );
$service_api_route = 'tests/' . $alert->screenshot_test_id . '/false-positives';
return self::rest_service_request( $service_api_route, [
'comparison_id' => $alert->comparison_id,
], 'post' );
}
/**
* Unmark alert as false positive.
*
* @param int $alert_id Alert id.
*/
public static function unmark_alert_as_false_positive( $alert_id ) {
$alert = Alert::get_item( $alert_id );
$service_api_route = 'tests/' . $alert->screenshot_test_id . '/false-positives/' . $alert->comparison_id;
return self::rest_service_request( $service_api_route, [], 'delete' );
}
/**
* Get project id from the service.
*/
public static function fetch_updates() {
$service_project_id = get_option( 'vrts_project_id' );
$service_api_route = 'sites/' . $service_project_id . '/updates';
return self::rest_service_request( $service_api_route, [], 'get' );
}
/**
* Get test runs from the service.
*
* @param string[] $test_run_ids the test run ids.
*/
public static function fetch_test_runs( $test_run_ids ) {
$service_project_id = get_option( 'vrts_project_id' );
$service_api_route = 'sites/' . $service_project_id . '/runs';
$service_api_route = add_query_arg( 'ids', implode( ',', $test_run_ids ), $service_api_route );
return self::rest_service_request( $service_api_route, [], 'get' );
}
/**
* Delete project from the service.
*/
public static function disconnect_service() {
$service_project_id = get_option( 'vrts_project_id' );
$service_api_route = 'sites/' . $service_project_id;
$response = self::rest_service_request( $service_api_route, [], 'delete' );
if ( 200 === $response['status_code'] ) {
update_option( 'vrts_disconnected', 1 );
}
}
/**
* Drop the database table for tests.
*/
public static function delete_option() {
delete_option( 'vrts_project_id' );
delete_option( 'vrts_project_token' );
delete_option( 'vrts_project_secret' );
delete_option( 'vrts_create_token' );
delete_option( 'vrts_access_token' );
delete_option( 'vrts_homepage_added' );
delete_option( 'vrts_site_urls' );
delete_option( 'vrts_connection_inactive' );
delete_option( self::SERVICE . '_version' );
}
/**
* Check if external service was able to connect
*/
public static function is_connected() {
return ! (bool) get_option( 'vrts_disconnected' ) && (bool) get_option( 'vrts_project_id' ) && (bool) get_option( 'vrts_project_token' );
}
/**
* Check if secret was created
*/
public static function has_secret() {
return (bool) get_option( 'vrts_project_secret' );
}
/**
* Set user agent for the request.
*
* @param string $user_agent the user agent.
*/
public static function set_user_agent( $user_agent ) {
if ( function_exists( 'vrts' ) ) {
return 'VRTs/' . vrts()->get_plugin_info( 'version' ) . ';' . $user_agent;
} else {
return $user_agent;
}
}
/**
* Create secret for the project
*/
private static function create_secret() {
$service_project_id = get_option( 'vrts_project_id' );
$service_api_route = 'sites/' . $service_project_id . '/secret';
$service_request = self::rest_service_request( $service_api_route, [], 'post' );
if ( 200 === $service_request['status_code'] ) {
update_option( 'vrts_project_secret', $service_request['response']['secret'] ?? null );
}
}
}
@@ -0,0 +1,451 @@
<?php
namespace Vrts\Features;
use Vrts\Core\Utilities\Sanitization;
use Vrts\Core\Utilities\Url_Helpers;
use Vrts\Features\Subscription;
use Vrts\Services\Test_Service;
class Settings_Page {
/**
* Page slug.
*
* @var string
*/
protected $page_slug = 'vrts-settings';
/**
* Constructor.
*/
public function __construct() {
add_action( 'init', [ $this, 'add_settings' ] );
add_action( 'admin_menu', [ $this, 'add_submenu_page' ] );
add_action( 'admin_init', [ $this, 'settings_migration' ] );
add_action( 'add_option_vrts_click_selectors', [ $this, 'do_after_update_click_selectors' ], 10, 2 );
add_action( 'update_option_vrts_click_selectors', [ $this, 'do_after_update_click_selectors' ], 10, 2 );
add_action( 'pre_update_option_vrts_license_key', [ $this, 'do_before_add_license_key' ], 10, 2 );
add_action( 'pre_update_option_vrts_email_update_notification_address', [ $this, 'do_before_updating_email_address' ], 10 );
add_action( 'pre_update_option_vrts_email_api_notification_address', [ $this, 'do_before_updating_email_address' ], 10 );
add_action( 'update_option_vrts_automatic_comparison', [ $this, 'do_after_update_vrts_automatic_comparison' ], 10, 2 );
}
/**
* Add submenu.
*/
public function add_submenu_page() {
$submenu_page = add_submenu_page(
'vrts',
esc_html__( 'Settings', 'visual-regression-tests' ),
esc_html__( 'Settings', 'visual-regression-tests' ),
'manage_options',
$this->page_slug,
[ $this, 'render_page' ]
);
add_action( 'load-' . $submenu_page, [ $this, 'init_notifications' ] );
}
/**
* Render settings page.
*/
public function render_page() {
vrts()->component( 'settings-page', [
'title' => esc_html__( 'Settings', 'visual-regression-tests' ),
'settings_fields' => $this->page_slug,
'settings_sections' => $this->page_slug,
] );
}
/**
* Register settings.
*/
public function add_settings() {
$has_subscription = (bool) Subscription::get_subscription_status();
vrts()->settings()->add_section([
'id' => 'vrts-settings-section-general',
'page' => $this->page_slug,
'title' => '',
]);
// Notice:
// value_type can be one of these 'string', 'boolean', 'integer', 'number', 'array', and 'object'.
// sanitize_callback can be a default wp sanitize function or a custom function from the Sanitization class.
// 'sanitize_callback' => '[ Sanitization::class, 'sanitize_checkbox' ]'.
vrts()->settings()->add_setting([
'type' => 'text',
'id' => 'vrts_click_selectors',
'section' => 'vrts-settings-section-general',
'title' => esc_html__( 'Click Element', 'visual-regression-tests' ),
'description' => sprintf(
'%s<br>%s',
sprintf(
/* translators: %s: link wrapper. */
esc_html__( 'Add a %1$sCSS selector%2$s to click on the first element found before creating a new snapshot.', 'visual-regression-tests' ),
'<a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors" target="_blank">',
'</a>'
),
esc_html__( 'Useful to accept cookie banners or anything else that should be clicked after page load.', 'visual-regression-tests' )
),
'placeholder' => esc_html__( 'e.g.: #accept-cookies', 'visual-regression-tests' ),
'sanitize_callback' => 'sanitize_text_field',
'show_in_rest' => true,
'value_type' => 'string',
'default' => '',
]);
vrts()->settings()->add_setting([
'type' => 'text',
'id' => 'vrts_license_key',
'section' => 'vrts-settings-section-general',
'title' => esc_html__( 'License Key', 'visual-regression-tests' ),
'description' => sprintf(
'%1$s <a href="%2$s" title="%3$s">%3$s</a>',
esc_html__( 'No license key yet?', 'visual-regression-tests' ),
esc_url( Url_Helpers::get_page_url( 'upgrade' ) ),
esc_html__( 'Upgrade here.', 'visual-regression-tests' )
),
'placeholder' => esc_html_x( 'XXXXXXXX-XXXXXXXX-XXXXXXXX-XXXXXXXX', 'license key placeholder', 'visual-regression-tests' ),
'sanitize_callback' => 'sanitize_text_field',
'show_in_rest' => true,
'value_type' => 'string',
'value' => ! empty( vrts()->settings()->get_option( 'vrts_license_key', false ) ) ? 'XXXXXXXX-XXXXXXXX-XXXXXXXX-XXXXXXXX' : '',
'default' => '',
]);
vrts()->settings()->add_section([
'id' => 'vrts-settings-section-triggers',
'page' => $this->page_slug,
'title' => esc_html__( 'Triggers', 'visual-regression-tests' ) . '<span>' . esc_html__( 'When your Tests are run.', 'visual-regression-tests' ) . '</span>',
]);
vrts()->settings()->add_setting([
'type' => 'checkbox',
'id' => 'vrts_automatic_comparison',
'section' => 'vrts-settings-section-triggers',
'title' => esc_html__( 'Schedule', 'visual-regression-tests' ),
'label' => esc_html__( 'Run Tests every 24 hours.', 'visual-regression-tests' ),
'sanitize_callback' => [ Sanitization::class, 'sanitize_checkbox' ],
'show_in_rest' => true,
'value_type' => 'boolean',
'value' => 1,
'default' => 1,
'disabled' => 1,
]);
vrts()->settings()->add_setting([
'type' => 'checkbox',
'id' => 'vrts_updates_comparison',
'section' => 'vrts-settings-section-triggers',
'title' => esc_html__( 'Update', 'visual-regression-tests' ),
'label' => esc_html__( 'Run Tests after WordPress and plugin updates.', 'visual-regression-tests' ),
'sanitize_callback' => [ Sanitization::class, 'sanitize_checkbox' ],
'show_in_rest' => true,
'value_type' => 'boolean',
'value' => $has_subscription,
'default' => $has_subscription,
'disabled' => $has_subscription,
'readonly' => ! $has_subscription,
'is_pro' => $has_subscription,
]);
vrts()->settings()->add_setting([
'type' => 'checkbox',
'id' => 'vrts_api_comparison',
'section' => 'vrts-settings-section-triggers',
'title' => esc_html__( 'API', 'visual-regression-tests' ),
'label' => sprintf(
/* translators: %1$s, %2$s: link wrapper. */
wp_kses_post( __( 'Run Tests with your favorite apps. %1$sRead the docs%2$s.', 'visual-regression-tests' ) ),
'<a href="' . esc_url( 'https://vrts.app/docs/' ) . '" target="_blank">',
'</a>'
),
'sanitize_callback' => [ Sanitization::class, 'sanitize_checkbox' ],
'show_in_rest' => true,
'value_type' => 'boolean',
'value' => $has_subscription,
'default' => $has_subscription,
'disabled' => $has_subscription,
'readonly' => ! $has_subscription,
'is_pro' => $has_subscription,
]);
vrts()->settings()->add_setting([
'type' => 'checkbox',
'id' => 'vrts_manual_comparison',
'section' => 'vrts-settings-section-triggers',
'title' => esc_html__( 'Manual', 'visual-regression-tests' ),
'label' => esc_html__( 'Run Tests on demand.', 'visual-regression-tests' ),
'sanitize_callback' => [ Sanitization::class, 'sanitize_checkbox' ],
'show_in_rest' => true,
'value_type' => 'boolean',
'value' => $has_subscription,
'default' => $has_subscription,
'disabled' => $has_subscription,
'readonly' => ! $has_subscription,
'is_pro' => $has_subscription,
]);
vrts()->settings()->add_section([
'id' => 'vrts-settings-section-notifications',
'page' => $this->page_slug,
'title' => esc_html__( 'Notifications', 'visual-regression-tests' ) . '<span>' . esc_html__( 'Notify team members based on specific Trigger events.', 'visual-regression-tests' ) . '</span>',
]);
vrts()->settings()->add_setting([
'type' => 'text',
'id' => 'vrts_email_notification_address',
'section' => 'vrts-settings-section-notifications',
'title' => esc_html__( 'Schedule', 'visual-regression-tests' ),
'description' => esc_html__( 'Separate multiple email addresses with commas. Or leave blank to disable notifications.', 'visual-regression-tests' ),
'placeholder' => esc_html__( 'Email address(es)', 'visual-regression-tests' ),
'sanitize_callback' => [ Sanitization::class, 'sanitize_multiple_emails' ],
'show_in_rest' => true,
'value_type' => 'string',
'default' => get_bloginfo( 'admin_email' ),
'return_value_callback' => [ $this, 'get_sanitized_emails' ],
]);
vrts()->settings()->add_setting([
'type' => 'text',
'id' => 'vrts_email_update_notification_address',
'section' => 'vrts-settings-section-notifications',
'title' => esc_html__( 'Update', 'visual-regression-tests' ),
'placeholder' => esc_html__( 'Email address(es)', 'visual-regression-tests' ),
'sanitize_callback' => [ Sanitization::class, 'sanitize_multiple_emails' ],
'show_in_rest' => true,
'value_type' => 'string',
'default' => '',
'readonly' => ! $has_subscription,
'is_pro' => $has_subscription,
'return_value_callback' => [ $this, 'get_sanitized_emails' ],
]);
vrts()->settings()->add_setting([
'type' => 'text',
'id' => 'vrts_email_api_notification_address',
'section' => 'vrts-settings-section-notifications',
'title' => esc_html__( 'API', 'visual-regression-tests' ),
'placeholder' => esc_html__( 'Email address(es)', 'visual-regression-tests' ),
'sanitize_callback' => [ Sanitization::class, 'sanitize_multiple_emails' ],
'show_in_rest' => true,
'value_type' => 'string',
'default' => '',
'readonly' => ! $has_subscription,
'is_pro' => $has_subscription,
'return_value_callback' => [ $this, 'get_sanitized_emails' ],
]);
vrts()->settings()->add_setting([
'type' => 'info',
'id' => 'vrts_email_manual_notification_address',
'section' => 'vrts-settings-section-notifications',
'title' => esc_html__( 'Manual', 'visual-regression-tests' ),
'description' => esc_html__( 'Alerts are automatically sent to the user who triggers the Manual Test.', 'visual-regression-tests' ),
'is_pro' => $has_subscription,
]);
}
/**
* Settings migration.
*/
public function settings_migration() {
$has_subscription = (bool) Subscription::get_subscription_status();
$old_cc_email = get_option( 'vrts_email_notification_cc_address' );
$update_email = get_option( 'vrts_email_update_notification_address' );
$api_email = get_option( 'vrts_email_api_notification_address' );
if ( $old_cc_email ) {
$old_cc_email = $this->get_sanitized_emails( $old_cc_email );
$schedule_email = vrts()->settings()->get_option( 'vrts_email_notification_address' );
$schedule_email = array_unique( array_merge( $schedule_email, $old_cc_email ) );
$schedule_email = implode( ', ', $schedule_email );
update_option( 'vrts_email_notification_address', $schedule_email );
delete_option( 'vrts_email_notification_cc_address' );
}
if ( $has_subscription ) {
$schedule_email = vrts()->settings()->get_option( 'vrts_email_notification_address', false );
if ( false === $update_email ) {
update_option( 'vrts_email_update_notification_address', $schedule_email );
}
if ( false === $api_email ) {
update_option( 'vrts_email_api_notification_address', $schedule_email );
}
if ( get_option( 'vrts_license_success' ) ) {
update_option( 'vrts_email_update_notification_address', $schedule_email );
update_option( 'vrts_email_api_notification_address', $schedule_email );
}
}
}
/**
* Update global click selector settings for project in service
*
* @param string $old Old value.
* @param string $new_value New value.
*/
public function do_after_update_click_selectors( $old, $new_value ) {
if ( $old !== $new_value ) {
$service_project_id = get_option( 'vrts_project_id' );
$service_api_route = 'sites/' . $service_project_id;
$parameters = [
'screenshot_options' => [
'clickSelectors' => $new_value,
],
];
$response = Service::rest_service_request( $service_api_route, $parameters, 'put' );
$service = new Test_Service();
$service->resume_tests();
}
}
/**
* Register the Gumroad API key with the service.
*
* @param mixed $new_value new value.
* @param mixed $old old value.
*/
public function do_before_add_license_key( $new_value, $old ) {
// If license key is empty but was previously added.
if ( ! $new_value && $old ) {
self::remove_license_key();
update_option( 'vrts_license_failed', true );
return '';
}
if ( 'XXXXXXXX-XXXXXXXX-XXXXXXXX-XXXXXXXX' === $new_value ) {
return $old;
}
if ( $old !== $new_value ) {
$service_project_id = get_option( 'vrts_project_id' );
$service_api_route = 'sites/' . $service_project_id . '/register';
$parameters = [
'license_key' => $new_value,
];
$response = Service::rest_service_request( $service_api_route, $parameters, 'post' );
$status_code = $response['status_code'];
Subscription::get_latest_status();
if ( 200 !== $status_code ) {
// If new key is not valid, remove the old one.
self::remove_license_key();
update_option( 'vrts_license_failed', true );
return '';
}
update_option( 'vrts_license_success', true );
return $new_value;
}//end if
return $old;
}
/**
* Prevent updating email address if there is no subscription
*
* @param string $value New value.
*/
public function do_before_updating_email_address( $value ) {
$has_subscription = (bool) Subscription::get_subscription_status();
return $has_subscription ? $value : '';
}
/**
* Update automatic comparison settings for project in service
*
* @param string $old Old value.
* @param string $new_value New value.
*/
public function do_after_update_vrts_automatic_comparison( $old, $new_value ) {
if ( $old !== $new_value ) {
$service_project_id = get_option( 'vrts_project_id' );
$service_api_route = 'sites/' . $service_project_id;
$parameters = [
'automatic_comparison' => empty( $new_value ) ? false : true,
];
$response = Service::rest_service_request( $service_api_route, $parameters, 'put' );
}
}
/**
* Remove license key from the service
*/
public static function remove_license_key() {
$service_project_id = get_option( 'vrts_project_id' );
$service_api_route = 'sites/' . $service_project_id . '/unregister';
$response = Service::rest_service_request( $service_api_route, [], 'post' );
Subscription::get_latest_status();
}
/**
* Init notifications.
*/
public function init_notifications() {
if ( true === (bool) get_option( 'vrts_license_success' ) ) {
add_action( 'admin_notices', [ $this, 'render_notification_license_added' ] );
delete_option( 'vrts_license_success' );
} elseif ( true === (bool) get_option( 'vrts_license_failed' ) ) {
add_action( 'admin_notices', [ $this, 'render_notification_license_not_added' ] );
delete_option( 'vrts_license_failed' );
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- It's OK.
} elseif ( isset( $_GET['settings-updated'] ) && true === (bool) $_GET['settings-updated'] ) {
add_action( 'admin_notices', [ $this, 'render_notification_settings_saved' ] );
}
}
/**
* Render Settings saved notification.
*/
public function render_notification_settings_saved() {
Admin_Notices::render_notification( 'settings_saved', false );
}
/**
* Render License added notification.
*/
public function render_notification_license_added() {
Admin_Notices::render_notification( 'license_added', false );
}
/**
* Render License adding failed notification.
*/
public function render_notification_license_not_added() {
Admin_Notices::render_notification( 'license_not_added', false );
}
/**
* Render License adding removed notification.
*/
public function render_notification_license_removed() {
Admin_Notices::render_notification( 'license_removed', false );
}
/**
* Sanitize multiple emails.
*
* @param string $emails The emails.
*
* @return array
*/
public function get_sanitized_emails( $emails ) {
return array_filter( array_map( 'sanitize_email', explode( ',', $emails ) ) );
}
}
@@ -0,0 +1,160 @@
<?php
namespace Vrts\Features;
use Vrts\Features\Service;
use Vrts\Models\Test;
class Subscription {
/**
* Update the number of tests available.
*
* @param mixed $remaining_tests Option value. Must be serializable if non-scalar. Expected to not be SQL-escaped.
* @param mixed $available_tests Option value. Must be serializable if non-scalar. Expected to not be SQL-escaped.
* @param mixed $has_subscription Option value. Must be serializable if non-scalar. Expected to not be SQL-escaped.
* @param mixed $tier_id Option value. Must be serializable if non-scalar. Expected to not be SQL-escaped.
*/
public static function update_available_tests( $remaining_tests = null, $available_tests = null, $has_subscription = null, $tier_id = null ) {
if ( null !== $remaining_tests ) {
update_option( 'vrts_remaining_tests', $remaining_tests );
}
if ( null !== $available_tests ) {
update_option( 'vrts_total_tests', $available_tests );
}
if ( null !== $has_subscription ) {
update_option( 'vrts_has_subscription', (int) $has_subscription );
}
if ( null !== $tier_id ) {
update_option( 'vrts_tier_id', $tier_id );
}
}
/**
* Get the number of tests remaining.
*/
public static function get_remaining_tests() {
return get_option( 'vrts_remaining_tests' );
}
/**
* Get the number of total tests.
*/
public static function get_total_tests() {
return get_option( 'vrts_total_tests' );
}
/**
* Get subscription status.
*/
public static function get_subscription_status() {
return get_option( 'vrts_has_subscription' );
}
/**
* Get subscription tier id.
*/
public static function get_subscription_tier_id() {
return get_option( 'vrts_tier_id' );
}
/**
* Increase number of tests until server updates the number of available tests.
*
* @param int $number Number of tests to increase.
*/
public static function increase_tests_count( $number = 1 ) {
$remaining_tests = get_option( 'vrts_remaining_tests' );
$total_tests = get_option( 'vrts_total_tests' );
if ( $remaining_tests < $total_tests ) {
$remaining_tests += $number;
update_option( 'vrts_remaining_tests', min( $total_tests, $remaining_tests ) );
}
return true;
}
/**
* Decrease number of tests until server updates the number of available tests.
*
* @param int $number Number of tests to decrease.
*/
public static function decrease_tests_count( $number = 1 ) {
$remaining_tests = get_option( 'vrts_remaining_tests' );
if ( $remaining_tests >= 1 ) {
$remaining_tests -= $number;
update_option( 'vrts_remaining_tests', max( 0, $remaining_tests ) );
}
}
/**
* Drop the keys for subscription.
*/
public static function delete_options() {
delete_option( 'vrts_email_notification_address' );
delete_option( 'vrts_email_notification_cc_address' );
delete_option( 'vrts_email_update_notification_address' );
delete_option( 'vrts_email_api_notification_address' );
delete_option( 'vrts_email_manual_notification_address' );
delete_option( 'vrts_click_selectors' );
delete_option( 'vrts_license_key' );
delete_option( 'vrts_automatic_comparison' );
delete_option( 'vrts_updates_comparison' );
delete_option( 'vrts_remaining_tests' );
delete_option( 'vrts_total_tests' );
delete_option( 'vrts_has_subscription' );
delete_option( 'vrts_tier_id' );
}
/**
* Send request to server to get the subscription and tests status.
*/
public static function get_latest_status() {
$local_test_ids = Test::get_all_service_test_ids();
$service_project_id = get_option( 'vrts_project_id' );
$service_api_route = 'sites/' . $service_project_id;
$response = Service::rest_service_request( $service_api_route, [], 'get' );
$remaining_credits = $response['response']['remaining_credits'];
$total_credits = $response['response']['total_credits'];
$has_subscription = $response['response']['has_subscription'];
$tier_id = $response['response']['tier_id'];
// Active test ids returned by service.
$active_test_ids = $response['response']['active_test_ids'];
$paused_test_ids = $response['response']['paused_test_ids'];
$local_only_test_ids = [];
foreach ( $local_test_ids as $test_id ) {
if ( ! $has_subscription ) {
// phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict -- This is a loose comparison by design.
if ( ! in_array( $test_id, $active_test_ids ) && in_array( $test_id, $paused_test_ids ) ) {
Test::pause( $test_id );
}
// phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict -- This is a loose comparison by design.
} elseif ( in_array( $test_id, $paused_test_ids ) ) {
$service_api_route = 'tests/' . $test_id . '/resume';
$response = Service::rest_service_request( $service_api_route, [], 'post' );
Test::unpause( $test_id );
}
// phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict -- This is a loose comparison by design.
if ( $test_id && ! in_array( $test_id, $active_test_ids ) && ! in_array( $test_id, $paused_test_ids ) ) {
$local_only_test_ids[] = $test_id;
}
}
if ( ! empty( $local_only_test_ids ) ) {
Test::clear_remote_test_ids( $local_only_test_ids );
}
if ( array_key_exists( 'status_code', $response ) && 200 === $response['status_code'] ) {
if ( array_key_exists( 'response', $response ) ) {
self::update_available_tests( $remaining_credits, $total_credits, $has_subscription, $tier_id );
}
}
}
}
@@ -0,0 +1,251 @@
<?php
namespace Vrts\Features;
use Vrts\Core\Utilities\Url_Helpers;
use Vrts\List_Tables\Test_Runs_List_Table;
use Vrts\List_Tables\Test_Runs_Queue_List_Table;
use Vrts\Models\Alert;
use Vrts\Models\Test;
use Vrts\Models\Test_Run;
use Vrts\Services\Test_Run_Service;
class Test_Runs_Page {
/**
* Constructor.
*/
public function __construct() {
add_action( 'admin_init', [ $this, 'remove_admin_notices' ], 99 );
add_action( 'admin_menu', [ $this, 'add_submenu_page' ] );
add_action( 'admin_body_class', [ $this, 'add_body_class' ] );
}
/**
* Remove admin notices.
*/
public function remove_admin_notices() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verification is not required here.
if ( isset( $_GET['page'] ) && 'vrts-runs' === $_GET['page'] && isset( $_GET['run_id'] ) ) {
remove_all_actions( 'admin_notices' );
}
}
/**
* Add submenu.
*/
public function add_submenu_page() {
$count = Alert::get_total_items_grouped_by_test_run();
$submenu_page = add_submenu_page(
'vrts',
__( 'Runs', 'visual-regression-tests' ),
$count ? esc_html__( 'Runs', 'visual-regression-tests' ) . '&nbsp;<span class="update-plugins" title="' . esc_attr( $count ) . '">' . esc_html( $count ) . '</span>' : esc_html__( 'Runs', 'visual-regression-tests' ),
'manage_options',
'vrts-runs',
[ $this, 'render_page' ],
1
);
//phpcs:ignore WordPress.Security.NonceVerification.Recommended -- It's ok.
if ( ! isset( $_GET['run_id'] ) ) {
add_action( 'load-' . $submenu_page, [ $this, 'screen_option' ] );
add_action( 'load-' . $submenu_page, [ $this, 'init_notifications' ] );
}
}
/**
* Add screen options.
*/
public function screen_option() {
// Set Screen Option.
$option = 'per_page';
$args = [
'default' => 20,
'option' => 'vrts_test_runs_per_page',
];
// screen_option are user meta.
add_screen_option( $option, $args );
}
/**
* Add body class.
*
* @param string $classes Body classes.
*/
public function add_body_class( $classes ) {
//phpcs:ignore WordPress.Security.NonceVerification.Recommended -- It's ok.
if ( isset( $_GET['run_id'] ) ) {
$classes .= ' vrts-test-run-wrap';
}
return $classes;
}
/**
* Render page
*/
public function render_page() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- It's ok.
$run_id = intval( $_GET['run_id'] ?? 0 );
$run = Test_Run::get_item( $run_id );
if ( $run ) {
$tests = $this->prepare_tests( maybe_unserialize( $run->tests ) );
$alerts = $this->prepare_alerts( $run_id, $tests );
list($alert_id, $alert) = $this->get_alert( $alerts );
$test = $alert ? Test::get_item_by_post_id( $alert->post_id ) : null;
$is_receipt = 'receipt' === $alert_id;
list( $current_pagination, $prev_alert_id, $next_alert_id ) = $this->get_pagination( $alerts, $alert_id );
vrts()->component('test-run-page', [
'run' => $run,
'alerts' => $alerts,
'alert' => $alert,
'is_receipt' => $is_receipt,
'pagination' => [
'prev_alert_id' => $prev_alert_id,
'next_alert_id' => $next_alert_id,
'current' => $current_pagination,
'total' => count( $alerts ),
],
'tests' => $tests,
'test_settings' => [
'test_id' => isset( $test->id ) ? $test->id : null,
'hide_css_selectors' => isset( $test->hide_css_selectors ) ? $test->hide_css_selectors : null,
],
]);
} else {
vrts()->component('test-runs-page', [
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- It's ok.
'search_query' => sanitize_text_field( wp_unslash( $_POST['s'] ?? '' ) ),
'list_table' => new Test_Runs_List_Table(),
'list_queue_table' => new Test_Runs_Queue_List_Table(),
]);
}//end if
}
/**
* Prepare alerts.
*
* @param array $tests Tests.
*/
private function prepare_tests( $tests ) {
if ( is_array( $tests ) && count( $tests ) > 0 && ! is_array( $tests[0] ) ) {
$tests = array_map( function ( $test ) {
$test = (int) $test;
$post_id = Test::get_post_id( $test );
return [
'id' => $test,
'post_id' => $post_id,
'post_title' => get_the_title( $post_id ),
'permalink' => get_permalink( $post_id ),
];
}, $tests );
}
usort( $tests, function ( $a, $b ) {
return $a['id'] > $b['id'] ? -1 : 1;
} );
return $tests;
}
/**
* Prepare alerts.
*
* @param int $run_id Run ID.
* @param array $tests Tests.
*/
private function prepare_alerts( $run_id, $tests ) {
$alerts = Alert::get_items_by_test_run( $run_id );
$alerts_by_post_id = [];
foreach ( $alerts as $alert ) {
$alerts_by_post_id[ $alert->post_id ][] = $alert;
}
$sorted_alerts = [];
foreach ( $tests as $test ) {
if ( isset( $alerts_by_post_id[ $test['post_id'] ] ) ) {
$sorted_alerts = array_merge( $sorted_alerts, $alerts_by_post_id[ $test['post_id'] ] );
unset( $alerts_by_post_id[ $test['post_id'] ] );
}
}
$remaining_alerts = array_values( $alerts_by_post_id );
usort( $remaining_alerts, function ( $a, $b ) {
return $a[0]->post_id > $b[0]->post_id ? -1 : 1;
} );
foreach ( $remaining_alerts as $remaining_alert ) {
$sorted_alerts = array_merge( $sorted_alerts, $remaining_alert );
}
return $sorted_alerts;
}
/**
* Get pagination.
*
* @param array $alerts Alerts.
* @param int $alert_id Alert ID.
*/
private function get_pagination( $alerts, $alert_id ) {
if ( 'receipt' === $alert_id ) {
$current_pagination = count( $alerts );
$prev_alert_id = $alerts[ count( $alerts ) - 1 ]->id ?? 0;
$next_alert_id = 0;
} else {
$current_index = ( array_keys( array_filter( $alerts, function ( $alert ) use ( $alert_id ) {
return $alert->id === $alert_id;
} ) )[0] ?? 0 );
$prev_alert_id = $alerts[ $current_index - 1 ]->id ?? 0;
$next_alert_id = $alerts[ $current_index + 1 ]->id ?? 0;
$current_pagination = $current_index + 1;
if ( ! $next_alert_id ) {
$next_alert_id = 'receipt';
}
}
return [ $current_pagination, $prev_alert_id, $next_alert_id ];
}
/**
* Get alert.
*
* @param array $alerts Alerts.
*/
private function get_alert( $alerts ) {
$first_alert_id = isset( $alerts[0] ) ? $alerts[0]->id : 0;
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verification is not required here.
$alert_id = sanitize_text_field( wp_unslash( $_GET['alert_id'] ?? $first_alert_id ) );
if ( 'receipt' === $alert_id ) {
return [ 'receipt', null ];
}
$alert = Alert::get_item( $alert_id );
if ( ! $alert ) {
$alert_id = $first_alert_id;
$alert = Alert::get_item( $alert_id );
}
return [ $alert_id, $alert ];
}
/**
* Init notifications.
*/
public function init_notifications() {
if ( ! Service::is_connected() ) {
add_action( 'admin_notices', [ $this, 'render_notification_connection_failed' ] );
}
}
/**
* Render connection_failed Notification.
*/
public function render_notification_connection_failed() {
Admin_Notices::render_notification( 'connection_failed' );
}
}
@@ -0,0 +1,499 @@
<?php
namespace Vrts\Features;
use Vrts\Core\Utilities\Url_Helpers;
use Vrts\List_Tables\Tests_List_Table;
use Vrts\Models\Test;
use Vrts\Features\Subscription;
use Vrts\Services\Test_Service;
use Vrts\Services\Manual_Test_Service;
class Tests_Page {
/**
* Constructor.
*/
public function __construct() {
add_action( 'admin_menu', [ $this, 'add_submenu_page' ] );
add_filter( 'set-screen-option', [ $this, 'set_screen' ], 10, 3 );
add_action( 'wp_link_query', [ $this, 'wp_link_query' ], 10, 2 );
add_action( 'wp_ajax_vrts_test_quick_edit_save', [ $this, 'quick_edit_save' ] );
add_action( 'admin_init', [ $this, 'handle_bulk_actions' ] );
}
/**
* Add submenu.
*/
public function add_submenu_page() {
$submenu_page = add_submenu_page(
'vrts',
__( 'Tests', 'visual-regression-tests' ),
__( 'Tests', 'visual-regression-tests' ),
'manage_options',
'vrts',
[ $this, 'render_page' ],
1
);
remove_submenu_page( 'vrts', 'vrts' );
add_action( 'load-' . $submenu_page, [ $this, 'add_assets' ] );
add_action( 'load-' . $submenu_page, [ $this, 'submit_add_new_test' ] );
add_action( 'load-' . $submenu_page, [ $this, 'submit_run_manual_tests' ] );
add_action( 'load-' . $submenu_page, [ $this, 'submit_retry_connection' ] );
add_action( 'load-' . $submenu_page, [ $this, 'process_column_actions' ] );
add_action( 'load-' . $submenu_page, [ $this, 'init_notifications' ] );
}
/**
* Screen Option
*
* @param mixed $status The value to save instead of the option value. Default false (to skip saving the current option).
* @param string $option The option name.
* @param int $value The option value.
* @return mixed
*/
public static function set_screen( $status, $option, $value ) {
return $value;
}
/**
* Render page
*/
public function render_page() {
vrts()->component('tests-page', [
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- It's ok.
'id' => intval( $_GET['id'] ?? 0 ),
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- It's ok.
'action' => sanitize_text_field( wp_unslash( $_GET['action'] ?? 'list' ) ),
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- It's ok.
'search_query' => sanitize_text_field( wp_unslash( $_POST['s'] ?? '' ) ),
'list_table' => new Tests_List_Table(),
'remaining_tests' => Subscription::get_remaining_tests(),
'is_connected' => Service::is_connected(),
'running_tests_count' => count( Test::get_all_running() ),
]);
}
/**
* Handle the submit of the Add New button.
*/
public function submit_add_new_test() {
if ( ! isset( $_POST['submit_add_new_test'] ) ) {
return;
}
if ( isset( $_POST['_wpnonce'] ) && ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ), 'vrts_page_tests_nonce' ) ) {
die( esc_html__( 'Are you cheating?', 'visual-regression-tests' ) );
}
if ( ! current_user_can( 'read' ) ) {
wp_die( esc_html__( 'Permission Denied!', 'visual-regression-tests' ) );
}
$errors = [];
$page_url = Url_Helpers::get_page_url( 'tests' );
$post_id = isset( $_POST['post_id'] ) ? sanitize_text_field( wp_unslash( $_POST['post_id'] ) ) : 0;
// Some basic validation.
if ( ! $post_id ) {
$errors[] = esc_html__( 'Error: Post ID is required.', 'visual-regression-tests' );
}
// Bail out if error found.
if ( $errors ) {
wp_safe_redirect( $page_url );
exit;
}
// New or edit?
if ( $post_id ) {
$test_service = new Test_Service();
$insert_test = $test_service->create_test( $post_id );
}
if ( is_wp_error( $insert_test ) ) {
$redirect_to = add_query_arg([
'new-test-failed' => true,
'post_id' => $post_id,
], $page_url);
} else {
$redirect_to = add_query_arg([
'message' => 'success',
'new-test-added' => true,
'post_id' => $post_id,
], $page_url);
}//end if
wp_safe_redirect( $redirect_to );
exit;
}
/**
* Handle the submit of the Run Manual Tests button.
*/
public function submit_run_manual_tests() {
if ( ! isset( $_POST['submit_run_manual_tests'] ) ) {
return;
}
if ( isset( $_POST['_wpnonce'] ) && ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ), 'submit_run_manual_tests' ) ) {
die( esc_html__( 'Are you cheating?', 'visual-regression-tests' ) );
}
if ( ! current_user_can( 'read' ) ) {
wp_die( esc_html__( 'Permission Denied!', 'visual-regression-tests' ) );
}
$service = new Manual_Test_Service();
$service->run_tests();
$page_url = Url_Helpers::get_page_url( 'runs' );
wp_safe_redirect( $page_url );
exit;
}
/**
* Handle the submit of the Retry connection button.
*/
public function submit_retry_connection() {
if ( ! isset( $_POST['submit_retry_connection'] ) ) {
return;
}
if ( isset( $_POST['_wpnonce'] ) && ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ), 'vrts_retry_connection_nonce' ) ) {
die( esc_html__( 'Are you cheating?', 'visual-regression-tests' ) );
}
if ( ! current_user_can( 'read' ) ) {
wp_die( esc_html__( 'Permission Denied!', 'visual-regression-tests' ) );
}
$response = Service::retry_connection();
$page_url = Url_Helpers::get_page_url( 'tests' );
wp_safe_redirect( $page_url );
exit;
}
/**
* Handle the submit of process_column_actions.
*/
public function process_column_actions() {
if ( ! isset( $_GET['action'] ) && ! isset( $_GET['test_id'] ) ) {
return;
}
if ( isset( $_POST['_wpnonce'] ) && ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ), 'vrts_page' ) ) {
die( esc_html__( 'Are you cheating?', 'visual-regression-tests' ) );
}
if ( ! current_user_can( 'read' ) ) {
wp_die( esc_html__( 'Permission Denied!', 'visual-regression-tests' ) );
}
$errors = [];
$page_url = Url_Helpers::get_page_url( 'tests' );
$test_id = isset( $_GET['test_id'] ) ? sanitize_text_field( wp_unslash( $_GET['test_id'] ) ) : 0;
$action = isset( $_GET['action'] ) ? sanitize_text_field( wp_unslash( $_GET['action'] ) ) : 0;
// some basic validation.
if ( ! $test_id ) {
$errors[] = esc_html__( 'Error: Test ID is required.', 'visual-regression-tests' );
}
// bail out if error found.
if ( $errors ) {
$first_error = reset( $errors );
$redirect_to = add_query_arg( [ 'error' => $first_error ], $page_url );
wp_safe_redirect( $redirect_to );
exit;
}
// Disable Testing.
if ( $test_id && 'disable-testing' === $action ) {
$test = Test::get_item( $test_id );
$service = new Test_Service();
$deleted = $service->delete_test( $test_id );
$redirect_to = add_query_arg([
'message' => 'success',
'testing-disabled' => ( Service::is_connected() ? true : false ),
'post_id' => $test->post_id,
], $page_url);
if ( ! $deleted ) {
$redirect_to = add_query_arg( [ 'message' => 'error' ], $page_url );
}
} elseif ( $test_id && 'run-manual-test' === $action ) {
$service = new Manual_Test_Service();
$service->run_tests( [ $test_id ] );
$test = Test::get_item( $test_id );
$redirect_to = add_query_arg([
'page' => 'vrts-runs',
'message' => 'success',
'run-manual-test' => true,
'post_id' => $test->post_id,
], $page_url);
}//end if
if ( empty( $redirect_to ) ) {
$redirect_to = add_query_arg( [ 'message' => 'error' ], $page_url );
}
wp_safe_redirect( $redirect_to );
exit;
}
/**
* This function is used to save hide css selectors in quick edit.
* It will be called from quick_edit_save function.
*
* @return json_decode
*/
public function quick_edit_save() {
if ( isset( $_POST['nonce'] ) && ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'vrts_test_quick_edit' ) ) {
exit( 'No naughty business please' );
}
if ( ! isset( $_POST['test_id'] ) || ! (int) $_POST['test_id'] ) {
exit( 'No naughty business please' );
}
$test_id = isset( $_POST['test_id'] ) ? (int) sanitize_text_field( wp_unslash( $_POST['test_id'] ) ) : 0;
$hide_css_selectors = isset( $_POST['hide_css_selectors'] ) ? sanitize_text_field( wp_unslash( $_POST['hide_css_selectors'] ) ) : '';
$current_hide_css_selectors = TEST::get_item( $test_id )->hide_css_selectors ?? '';
if ( $current_hide_css_selectors === $hide_css_selectors ) {
$response = [
'success' => true,
'message' => __( 'No changes made.', 'visual-regression-tests' ),
'hide_css_selectors' => $hide_css_selectors,
];
return wp_die( wp_json_encode( $response ) );
}
$test_service = new Test_Service();
$is_saved = $test_service->update_css_hide_selectors( $test_id, $hide_css_selectors );
if ( $is_saved && ! is_wp_error( $is_saved ) ) {
$success = true;
$message = __( 'Changes saved successfully.', 'visual-regression-tests' );
$post_id = Test::get_item( $test_id )->post_id;
$test_service->resume_test( $post_id );
} else {
$success = false;
$message = __( 'Error while saving the changes.', 'visual-regression-tests' );
}
$test = Test::get_item( $test_id );
$snapshot_status = ! $test->target_screenshot_finish_date ? esc_html__( 'In progress', 'visual-regression-tests' ) : null;
$response = [
'success' => $success,
'message' => $message,
'hide_css_selectors' => $hide_css_selectors,
'snapshot_status' => $snapshot_status,
];
return wp_die( wp_json_encode( $response ) );
}
/**
* Add required assets.
*/
public function add_assets() {
// Remove may previously enqueued wplink script.
wp_deregister_script( 'wplink' );
// Register custom wplink for the Add New functionality.
wp_register_script( 'vrts-wplink', vrts()->get_plugin_url( 'assets/scripts/wplink.js' ), [ 'jquery', 'wp-a11y' ], vrts()->get_plugin_info( 'version' ), false );
// Enqueue custom wplink.
wp_enqueue_script( 'vrts-wplink' );
// Localize custom wplink.
wp_localize_script(
'vrts-wplink',
'wpLinkL10n',
[
'noTitle' => esc_html__( '(no title)', 'visual-regression-tests' ),
'noMatchesFound' => esc_html__( 'No results to enable visual regression testing found.', 'visual-regression-tests' ),
/* translators: Minimum input length in characters to start searching posts in the "Insert/edit link" modal. */
'minInputLength' => (int) esc_html_x( '3', 'minimum input length for searching post links', 'visual-regression-tests' ),
]
);
wp_enqueue_style( 'editor-buttons' );
}
/**
* Modify wp_link_query results.
*
* @param array $results the results.
* @param array $query the query.
*
* @return array the modified results.
*/
public function wp_link_query( $results, $query ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- Required by wp_link_query filter signature.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Missing -- It should be ok here.
$is_vrts_filter_query = isset( $_POST['vrts_filter_query'] ) ? filter_var( wp_unslash( $_POST['vrts_filter_query'] ), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE ) : false;
if ( true === $is_vrts_filter_query ) {
foreach ( $results as &$result ) {
$result['run_tests_status'] = Test::exists_for_post( $result['ID'] );
}
}
return $results;
}
/**
* Init notifications.
*/
public function init_notifications() {
$total_test_items = Test::get_total_items();
$frontpage_id = get_option( 'page_on_front' );
$is_front_page_added = ! is_null( Test::get_item_id( $frontpage_id ) );
$is_connected = Service::is_connected();
if ( ! Service::is_connected() ) {
add_action( 'admin_notices', [ $this, 'render_notification_connection_failed' ] );
} elseif ( 0 === $total_test_items || ( 1 === $total_test_items && true === $is_front_page_added ) ) {
add_action( 'admin_notices', [ $this, 'render_notification_get_started' ] );
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- It should be ok here.
$is_new_test_added = isset( $_GET['new-test-added'] ) ? sanitize_text_field( wp_unslash( $_GET['new-test-added'] ) ) : false;
if ( $is_new_test_added ) {
add_action( 'admin_notices', [ $this, 'render_notification_new_test_added' ] );
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- It should be ok here.
$is_new_tests_added = isset( $_GET['new-tests-added'] ) ? sanitize_text_field( wp_unslash( $_GET['new-tests-added'] ) ) : false;
if ( $is_new_tests_added ) {
add_action( 'admin_notices', [ $this, 'render_notification_new_tests_added' ] );
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- It should be ok here.
$is_testing_disabled = isset( $_GET['testing-disabled'] ) ? sanitize_text_field( wp_unslash( $_GET['testing-disabled'] ) ) : false;
if ( $is_testing_disabled ) {
add_action( 'admin_notices', [ $this, 'render_notification_test_disabled' ] );
}
$remaining_tests = Subscription::get_remaining_tests();
if ( '1' === $remaining_tests ) {
add_action( 'admin_notices', [ $this, 'render_notification_unlock_more_tests' ] );
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- It should be ok here.
$is_new_test_failed = isset( $_GET['new-test-failed'] ) ? sanitize_text_field( wp_unslash( $_GET['new-test-failed'] ) ) : false;
if ( ( $is_new_test_failed || '0' === $remaining_tests ) && $is_connected ) {
add_action( 'admin_notices', [ $this, 'render_notification_new_test_failed' ] );
}
}
/**
* Render connection_failed Notification.
*/
public function render_notification_connection_failed() {
Admin_Notices::render_notification( 'connection_failed' );
}
/**
* Render get_started Notification.
*/
public function render_notification_get_started() {
Admin_Notices::render_notification( 'get_started', true );
}
/**
* Render new_test_added Notification.
*/
public function render_notification_new_test_added() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- It should be ok here.
$post_id = isset( $_GET['post_id'] ) ? intval( $_GET['post_id'] ) : false;
Admin_Notices::render_notification( 'new_test_added', false, [
'page_title' => get_the_title( $post_id ),
]);
}
/**
* Render new_tests_added Notification.
*/
public function render_notification_new_tests_added() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- It should be ok here.
$post_ids = isset( $_GET['post_ids'] ) ? array_map( 'intval', $_GET['post_ids'] ) : false;
if ( $post_ids ) {
Admin_Notices::render_notification( 'new_tests_added', false, [
'page_titles' => implode( ', ', array_reverse( array_map( function ( $post_id ) {
return get_the_title( $post_id );
}, $post_ids ) ) ),
]);
}
}
/**
* Render new_test_failed Notification.
*/
public function render_notification_new_test_failed() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- It should be ok here.
$post_id = isset( $_GET['post_id'] ) ? intval( $_GET['post_id'] ) : false;
Admin_Notices::render_notification( 'new_test_failed', false, [
'page_title' => get_the_title( $post_id ),
]);
}
/**
* Render test_disabled Notification.
*/
public function render_notification_test_disabled() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- It should be ok here.
$post_id = isset( $_GET['post_id'] ) ? intval( $_GET['post_id'] ) : false;
Admin_Notices::render_notification( 'test_disabled', false, [
'page_title' => get_the_title( $post_id ),
'post_id' => intval( $post_id ),
]);
}
/**
* Render unlock_more_tests Notification.
*/
public function render_notification_unlock_more_tests() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- It should be ok here.
Admin_Notices::render_notification( 'unlock_more_tests', false, [
'total_tests' => Subscription::get_total_tests(),
'remaining_tests' => Subscription::get_remaining_tests(),
]);
}
/**
* Handle bulk actions.
*/
public function handle_bulk_actions() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- It should be ok here.
$action = ! empty( $_REQUEST['action'] ) ? $_REQUEST['action'] : '';
// phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- It should be ok here.
$page = ! empty( $_REQUEST['page'] ) ? $_REQUEST['page'] : '';
if ( 'vrts-tests_list_table' === $page && 'run-manual-test' === $action ) {
$nonce = sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ?? '' ) );
if ( ! wp_verify_nonce( $nonce, 'bulk-tests' ) ) {
return;
}
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Should be okay for now.
$test_ids = wp_unslash( $_POST['id'] ?? 0 );
if ( 0 === $test_ids ) {
return;
}
$manual_test_service = new Manual_Test_Service();
$manual_test_service->run_tests( $test_ids );
$page_url = Url_Helpers::get_page_url( 'runs' );
wp_safe_redirect( $page_url );
exit;
}
}
}
@@ -0,0 +1,218 @@
<?php
namespace Vrts\Features;
use Vrts\Models\Test;
class Tests {
/**
* Constructor.
*/
public function __construct() {
// Allows developers to run tests by calling `do_action( 'vrts_run_tests' )`.
add_action( 'vrts_run_tests', [ $this, 'run_api_tests' ] );
add_action( 'upgrader_process_complete', [ $this, 'run_upgrader_tests' ], 10, 99 );
}
/**
* Run api tests.
*
* @param string $notes Notes.
*/
public static function run_api_tests( $notes = '' ) {
self::run_tests( 'api', $notes );
}
/**
* Run upgrader tests.
*
* @param \WP_Upgrader $upgrader Upgrader.
* @param array $options Options.
*/
public static function run_upgrader_tests( $upgrader, $options ) {
$updates = [];
if ( 'update' === $options['action'] ) {
switch ( $options['type'] ) {
case 'plugin':
if ( isset( $options['plugins'] ) ) {
if ( is_array( $options['plugins'] ) ) {
foreach ( $options['plugins'] as $plugin ) {
$updates[] = static::add_plugin( $plugin );
}
} else {
$updates[] = static::add_plugin( $options['plugins'] );
}
}
if ( isset( $options['plugin'] ) ) {
$updates[] = static::add_plugin( $options['plugin'] );
}
break;
case 'theme':
if ( isset( $options['themes'] ) ) {
if ( is_array( $options['themes'] ) ) {
foreach ( $options['themes'] as $theme ) {
$updates[] = static::add_theme( $theme );
}
} else {
$updates[] = static::add_theme( $options['themes'] );
}
}
if ( isset( $options['theme'] ) ) {
$updates[] = static::add_theme( $options['theme'] );
}
break;
case 'core':
$new_version = static::get_wp_version();
$updates[] = [
'type' => 'core',
'version' => $new_version,
];
break;
case 'translation':
if ( isset( $options['translations'] ) ) {
if ( is_array( $options['translations'] ) ) {
foreach ( $options['translations'] as $translation ) {
$updates[] = static::add_translation( $translation );
}
} else {
$updates[] = static::add_translation( $options['translations'] );
}
}
if ( isset( $options['translation'] ) ) {
$updates[] = static::add_translation( $options['translation'] );
}
break;
}//end switch
}//end if
if ( ! empty( $updates ) ) {
self::run_tests( 'update', null, $updates );
}
}
/**
* Prepare plugin for updates.
*
* @param string $plugin Plugin.
*
* @return array
*/
private static function add_plugin( $plugin ) {
$plugin_data = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin );
$new_version = $plugin_data['Version'];
$name = $plugin_data['Name'];
$slug = dirname( plugin_basename( $plugin ) );
return [
'type' => 'plugin',
'name' => $name,
'slug' => $slug,
'version' => $new_version,
];
}
/**
* Prepare theme for updates.
*
* @param string $theme Theme.
*
* @return array
*/
private static function add_theme( $theme ) {
$theme_data = wp_get_theme( $theme );
$new_version = $theme_data->get( 'Version' );
$name = $theme_data->get( 'Name' );
$slug = $theme;
return [
'type' => 'theme',
'name' => $name,
'slug' => $slug,
'version' => $new_version,
];
}
/**
* Prepare translation for updates.
*
* @param array $translation Translation.
*
* @return array
*/
private static function add_translation( $translation ) {
$type = $translation['type'];
$slug = $translation['slug'];
$language = $translation['language'];
if ( 'plugin' === $type ) {
$plugin_data = static::get_plugin_data( $slug );
$name = $plugin_data['Name'];
} elseif ( 'theme' === $type ) {
$theme_data = wp_get_theme( $slug );
$name = $theme_data->get( 'Name' );
} else {
$name = 'WordPress';
}
return [
'type' => $type,
'name' => $name,
'slug' => $slug,
'language' => $language,
];
}
/**
* Get plugin data.
*
* @param string $plugin_slug_or_file Plugin slug or file.
*
* @return array
*/
private static function get_plugin_data( $plugin_slug_or_file ) {
$plugin_file = WP_PLUGIN_DIR . '/' . $plugin_slug_or_file;
$plugin_data = get_plugin_data( $plugin_file );
if ( empty( $plugin_data['Name'] ) ) {
$plugins = get_plugins();
foreach ( $plugins as $file => $local_plugin_data ) {
$slug = dirname( $file );
if ( $slug === $plugin_slug_or_file ) {
$plugin_data = $local_plugin_data;
break;
}
}
}
return $plugin_data;
}
/**
* Run tests.
*
* @param string $trigger Trigger.
* @param string $trigger_notes Trigger notes.
* @param array $trigger_meta Trigger meta.
*/
private static function run_tests( $trigger, $trigger_notes, $trigger_meta = null ) {
$has_subscription = Subscription::get_subscription_status();
if ( ! $has_subscription ) {
return false;
}
$tests = Test::get_all_running();
$service_test_ids = wp_list_pluck( $tests, 'service_test_id' );
Service::run_manual_tests( $service_test_ids, [
'trigger' => $trigger,
'trigger_notes' => $trigger_notes,
'trigger_meta' => $trigger_meta,
] );
}
/**
* Get WordPress version.
*
* @return string
*/
public static function get_wp_version() {
return ( function () {
require ABSPATH . WPINC . '/version.php';
return $wp_version;
} )();
}
}
@@ -0,0 +1,27 @@
<?php
namespace Vrts\Features;
class Translations {
/**
* Constructor.
*/
public function __construct() {
add_action( 'init', [ $this, 'load_plugin_textdomain' ] );
add_action( 'init', [ $this, 'set_script_translations' ] );
}
/**
* Load Localisation files.
*/
public function load_plugin_textdomain() {
load_plugin_textdomain( 'visual-regression-tests', false, plugin_basename( dirname( VRTS_PLUGIN_FILE ) ) . '/languages' );
}
/**
* Set script translations.
*/
public function set_script_translations() {
wp_set_script_translations( 'vrts-editor', 'visual-regression-tests' );
}
}
@@ -0,0 +1,46 @@
<?php
namespace Vrts\Features;
use Vrts\Features\Subscription;
class Upgrade_Page {
/**
* Page slug.
*
* @var string
*/
protected $page_slug = 'vrts-upgrade';
/**
* Constructor.
*/
public function __construct() {
add_action( 'admin_menu', [ $this, 'add_submenu_page' ] );
}
/**
* Add submenu.
*/
public function add_submenu_page() {
add_submenu_page(
'vrts',
__( 'Upgrade', 'visual-regression-tests' ),
__( 'Upgrade', 'visual-regression-tests' ),
'manage_options',
$this->page_slug,
[ $this, 'render_page' ]
);
}
/**
* Render upgrade page.
*/
public function render_page() {
vrts()->component( 'upgrade-page', [
'title' => esc_html__( 'Upgrade', 'visual-regression-tests' ),
'has_subscription' => Subscription::get_subscription_status(),
'tier_id' => Subscription::get_subscription_tier_id(),
] );
}
}