Files
2026-07-02 15:54:39 -06:00

472 lines
14 KiB
PHP

<?php
namespace Vrts\Services;
use Vrts\Features\Cron_Jobs;
use Vrts\Features\Service;
use Vrts\Features\Subscription;
use Vrts\Models\Alert;
use Vrts\Models\Test;
use Vrts\Tables\Alerts_Table;
use Vrts\Tables\Tests_Table;
use WP_Error;
class Test_Service {
/**
* Update tests from comparison.
*
* @param int $alert_id Alert id.
* @param int $test_id Test id.
* @param array $data Comparison data.
*
* @return int|false
*/
public function update_test_from_comparison( $alert_id, $test_id, $data ) {
global $wpdb;
$table_test = Tests_Table::get_table_name();
$comparison = $data['comparison'];
if ( $comparison['screenshot']['image_url'] ?? null ) {
// Update test row with new id foreign key and add latest screenshot.
$update_data = [
'next_run_date' => $data['next_run_at'] ?? '',
'last_comparison_date' => $comparison['updated_at'],
'base_screenshot_url' => $comparison['screenshot']['image_url'],
'base_screenshot_date' => $comparison['screenshot']['updated_at'],
'is_running' => false,
];
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- It's ok.
return $wpdb->update(
$table_test,
$update_data,
[ 'service_test_id' => $test_id ]
);
}
}
/**
* Update test from schedule.
*
* @param int $test_id Test id.
* @param array $data Screenshot data.
*
* @return void
*/
public function update_test_from_schedule( $test_id, $data ) {
global $wpdb;
$table_test = Tests_Table::get_table_name();
$screenshot = $data['schedule']['base_screenshot'];
if ( $screenshot['image_url'] ?? null ) {
// Update test row with new id foreign key and add latest screenshot.
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- It's ok.
$wpdb->update(
$table_test,
[
'base_screenshot_url' => $screenshot['image_url'],
'base_screenshot_date' => $screenshot['updated_at'],
'next_run_date' => $data['next_run_at'] ?? '',
],
[ 'service_test_id' => $test_id ]
);
}
}
/**
* Create test from API data.
*
* @param array $data Data.
*
* @return int|false
*/
public function update_test_from_api_data( $data ) {
$test_id = $data['test_id'];
$post_id = Test::get_post_id_by_service_test_id( $test_id );
if ( $post_id ) {
if ( $data['schedule']['base_screenshot'] ?? null ) {
$this->update_test_from_schedule( $test_id, $data );
} elseif ( $data['comparison'] ?? null ) {
$alert_id = null;
if ( $data['comparison']['pixels_diff'] > 1 && ! $data['matches_false_positive'] ) {
$comparison = $data['comparison'];
$alert_service = new Alert_Service();
$alert_id = $alert_service->create_alert_from_comparison( $post_id, $test_id, $comparison );
}//end if
$test_service = new Test_Service();
$test_service->update_test_from_comparison( $alert_id, $test_id, $data );
}//end if
return true;
}//end if
}
/**
* Fetch and update tests.
*
* @return void
*/
public function fetch_and_update_tests() {
$service_request = Service::fetch_updates();
if ( 200 === $service_request['status_code'] ) {
$response = $service_request['response'];
if ( array_key_exists( 'updates', $response ) ) {
$updates = $response['updates'];
foreach ( $updates as $update ) {
$this->update_test_from_api_data( $update );
}
}
if (
array_key_exists( 'remaining_credits', $response )
&& array_key_exists( 'total_credits', $response )
&& array_key_exists( 'has_subscription', $response )
&& array_key_exists( 'tier_id', $response )
) {
Subscription::update_available_tests( $response['remaining_credits'], $response['total_credits'], $response['has_subscription'], $response['tier_id'] );
}
}
}
/**
* Create test.
*
* @param int $post_id Post id.
*
* @return int|WP_Error
*/
public function create_test( $post_id ) {
if ( Service::is_connected() ) {
$post = get_post( $post_id );
if ( ! $post ) {
return new WP_Error( 'vrts_post_error', __( 'Post not found.', 'visual-regression-tests' ) );
}
$test = Test::get_item_by_post_id( $post_id );
if ( $test ) {
return $test;
}
if ( 'publish' === $post->post_status ) {
return $this->create_remote_test( $post );
} elseif ( 'revision' !== $post->post_type && 'auto-draft' !== $post->post_status ) {
$args = [
'post_id' => $post_id,
'status' => 0,
];
$new_row_id = Test::save( $args );
return Test::get_item( $new_row_id );
}
} else {
return new WP_Error( 'vrts_service_error', __( 'Service is not connected.', 'visual-regression-tests' ) );
}//end if
return new WP_Error( 'vrts_service_error', __( 'Error creating test.', 'visual-regression-tests' ) );
}
/**
* Create tests.
*
* @param array $post_ids Post ids.
*
* @return array|WP_Error
*/
public function create_tests( $post_ids ) {
if ( Service::is_connected() ) {
$created_tests = [];
$post_types = vrts()->get_public_post_types();
$posts = get_posts( [
'post__in' => $post_ids,
'post_type' => $post_types,
'post_status' => 'any',
'posts_per_page' => -1,
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
] );
if ( ! $posts ) {
return new WP_Error( 'vrts_posts_error', __( 'Posts not found.', 'visual-regression-tests' ) );
}
$post_ids = wp_list_pluck( $posts, 'ID' );
$remaining_tests = get_option( 'vrts_remaining_tests' );
$existing_tests = Test::get_items_by_post_ids( $post_ids );
$existing_tests_posts_ids = array_map( 'intval', wp_list_pluck( $existing_tests, 'post_id' ) );
// Remove posts that already have tests.
$posts = array_filter( $posts, function ( $post ) use ( $existing_tests_posts_ids ) {
return ! in_array( $post->ID, $existing_tests_posts_ids, true );
} );
if ( ! $posts ) {
return $existing_tests;
}
// Only try to create as many remote tests as we have remaining.
$published_posts = array_slice( array_filter( $posts, function ( $post ) {
return 'publish' === $post->post_status;
} ), 0, $remaining_tests );
$not_published_posts = array_filter( $posts, function ( $post ) {
return 'publish' !== $post->post_status && 'revision' !== $post->post_type && 'auto-draft' !== $post->post_status;
} );
if ( $published_posts ) {
$published_posts_ids = wp_list_pluck( $published_posts, 'ID' );
$remote_tests = $this->create_remote_tests( $published_posts_ids );
if ( ! is_wp_error( $remote_tests ) ) {
$created_tests = $remote_tests;
}
}
if ( $not_published_posts ) {
$args = array_values( array_map(function ( $post ) {
return [
'post_id' => $post->ID,
'status' => 0,
];
}, $not_published_posts) );
$is_saved = Test::save_multiple( $args );
if ( $is_saved ) {
$not_published_posts_ids = wp_list_pluck( $not_published_posts, 'ID' );
$created_tests = array_merge( $created_tests, Test::get_items_by_post_ids( $not_published_posts_ids ) );
}
}
return $created_tests;
} else {
return new WP_Error( 'vrts_service_error', __( 'Service is not connected.', 'visual-regression-tests' ) );
}//end if
return new WP_Error( 'vrts_service_error', __( 'Error creating test.', 'visual-regression-tests' ) );
}
/**
* Create test remotely via service.
*
* @param WP_Post $post Post.
* @param array $test Test.
*
* @return object|WP_Error
*/
public function create_remote_test( $post, $test = [] ) {
if ( Service::is_connected() ) {
$existing_test = Test::get_item_by_post_id( $post->ID );
if ( $existing_test && $existing_test->service_test_id ) {
return $existing_test;
}
$service_project_id = get_option( 'vrts_project_id' );
$request_url = 'tests';
$parameters = [
'project_id' => $service_project_id,
'url' => get_permalink( $post ),
'frequency' => 'daily',
];
$service_request = Service::rest_service_request( $request_url, $parameters, 'post' );
if ( 201 === $service_request['status_code'] ) {
$test_id = $service_request['response']['id'];
$args = array_merge($test, [
'post_id' => $post->ID,
'service_test_id' => $test_id,
'status' => 1,
]);
unset( $args['id'] );
// TODO: Add some validation.
$new_row_id = Test::save( $args, $test['id'] ?? null );
if ( $new_row_id ) {
Subscription::decrease_tests_count();
Cron_Jobs::schedule_initial_fetch_test_updates( $new_row_id );
return Test::get_item( $new_row_id );
}
} else {
return new WP_Error( 'vrts_service_error', __( 'Service could not create test.', 'visual-regression-tests' ) );
}
} else {
return new WP_Error( 'vrts_service_error', __( 'Service is not connected.', 'visual-regression-tests' ) );
}//end if
}
/**
* Create tests remotely via service.
*
* @param array $post_ids Post IDs.
*
* @return array|WP_Error
*/
public function create_remote_tests( $post_ids ) {
if ( Service::is_connected() ) {
$service_project_id = get_option( 'vrts_project_id' );
$request_url = 'tests';
$urls = array_combine( $post_ids, array_map( function ( $post_id ) {
return get_permalink( $post_id );
}, $post_ids ) );
$parameters = [
'project_id' => $service_project_id,
'urls' => array_values( $urls ),
'frequency' => 'daily',
];
$service_request = Service::rest_service_request( $request_url, $parameters, 'post' );
if ( 201 === $service_request['status_code'] ) {
$args = [];
foreach ( $service_request['response'] as $test ) {
$args[] = [
'post_id' => array_search( $test['url'], $urls, true ),
'service_test_id' => $test['id'],
'status' => 1,
];
}
$saved_tests_number = Test::save_multiple( $args );
if ( $saved_tests_number ) {
Subscription::decrease_tests_count( $saved_tests_number );
$post_ids = wp_list_pluck( $args, 'post_id' );
$created_tests = Test::get_items_by_post_ids( $post_ids );
foreach ( $created_tests as $test ) {
Cron_Jobs::schedule_initial_fetch_test_updates( $test->id );
}
return $created_tests;
} else {
return new WP_Error( 'vrts_test_error', __( 'Plugin could not save tests.', 'visual-regression-tests' ) );
}
} else {
return new WP_Error( 'vrts_service_error', __( 'Service could not create tests.', 'visual-regression-tests' ) );
}//end if
} else {
return new WP_Error( 'vrts_service_error', __( 'Service is not connected.', 'visual-regression-tests' ) );
}//end if
}
/**
* Delete test.
*
* @param int|object $test_id Test id or test object.
*
* @return bool
*/
public function delete_test( $test_id ) {
$delete_locally = true;
$test = ( is_string( $test_id ) || is_int( $test_id ) )
? Test::get_item( (int) $test_id )
: $test_id;
if ( ! empty( $test->service_test_id ) ) {
$delete_locally = (bool) $this->delete_remote_test( $test_id );
}
if ( ! $delete_locally ) {
Subscription::get_latest_status();
}
return $delete_locally && Test::delete( $test->id );
}
/**
* Delete test remotely via service.
*
* @param int|object $test_id Test id or test object.
*
* @return bool
*/
public function delete_remote_test( $test_id ) {
if ( Service::is_connected() ) {
$test = ( is_string( $test_id ) || is_int( $test_id ) )
? Test::get_item( (int) $test_id )
: $test_id;
if (
Service::delete_test( $test->service_test_id )
&& Subscription::increase_tests_count()
) {
$args = (array) $test;
$args['status'] = 0;
$args['service_test_id'] = null;
unset( $args['id'] );
unset( $args['current_alert_id'] );
Test::save( $args, $test->id );
return $test;
} else {
return false;
}
}
}
/**
* Resume stale tests.
*/
public function resume_stale_tests() {
$stale_tests = Test::get_all_inactive();
foreach ( $stale_tests as $stale_test ) {
if ( Subscription::get_remaining_tests() > 0 ) {
$this->resume_stale_test( $stale_test );
}
}
}
/**
* Resume stale test.
*
* @param object $test Test.
*/
private function resume_stale_test( $test ) {
if ( empty( $test->service_test_id ) ) {
$post = get_post( $test->post_id );
if ( 'publish' === $post->post_status ) {
$this->create_remote_test( $post, (array) $test );
}
}
}
/**
* Update test.
*
* @param int $test_id Test id.
* @param string $css_hide_selector CSS selector to hide.
*
* @return int|WP_Error
*/
public function update_css_hide_selectors( $test_id, $css_hide_selector ) {
if ( Service::is_connected() ) {
$test = Test::get_item( $test_id );
if ( ! $test ) {
return new WP_Error( 'vrts_test_error', __( 'Test not found.', 'visual-regression-tests' ) );
}
$updated = Service::update_test(
$test->service_test_id,
[ 'options' => [ 'hideSelectors' => $css_hide_selector ] ]
);
if ( $updated ) {
return Test::save_hide_css_selectors( $test_id, $css_hide_selector );
} else {
return new WP_Error( 'vrts_service_error', __( 'Service could not update test.', 'visual-regression-tests' ) );
}
} else {
return new WP_Error( 'vrts_service_error', __( 'Service is not connected.', 'visual-regression-tests' ) );
}
}
/**
* Resume test.
*
* @param int $post_id Post id.
*/
public function resume_test( $post_id ) {
$test = Test::get_item_by_post_id( $post_id );
if ( ! $test ) {
return false;
} else {
Test::reset_base_screenshot( $test->id );
Service::resume_test( $post_id );
}
}
/**
* Resume tests.
*/
public function resume_tests() {
Test::reset_base_screenshots();
Service::resume_tests();
}
}