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,194 @@
@import "@wordpress/base-styles/colors.native"; // stylelint-disable-line scss/at-import-partial-extension
@import "@wordpress/base-styles/mixins";
@import "@wordpress/base-styles/breakpoints";
@import "@wordpress/base-styles/variables";
@import "@wordpress/base-styles/default-custom-properties";
.vrts-admin-header {
box-sizing: border-box;
background: $white;
border-bottom: 1px solid $light-gray-500;
margin-left: -20px;
padding-left: 20px;
padding-top: 0;
height: 54px;
display: flex;
gap: 25px;
@media (max-width: $break-small) {
height: calc(54px + 46px);
padding-top: 46px;
}
@media (max-width: $break-medium) {
margin-left: -10px;
padding-left: 10px;
}
+ #wpbody {
@media (max-width: 600px) {
padding-top: 0;
}
}
&__logo {
position: relative;
top: -1px;
display: flex;
gap: 8px;
align-items: center;
text-decoration: none;
&-icon {
width: 20px;
height: 20px;
svg {
path {
fill: $gray-700;
}
}
}
&-text {
display: inline-block;
font-size: 16px;
@media (max-width: $break-medium) {
display: none;
}
}
}
&__navigation {
font-size: 13px;
display: flex;
margin: 0;
white-space: nowrap;
@media (max-width: $break-medium) {
gap: 20px;
overflow: auto;
-ms-overflow-style: none;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}
&-item {
display: flex;
margin: 0;
text-align: center;
}
&-link {
position: relative;
padding: 16px 20px;
text-decoration: none;
&::after {
content: "";
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 3px;
background: var(--vrts-admin-theme-color);
opacity: 0;
transition: opacity 0.2s;
}
[aria-current="true"] & {
border-bottom-color: var(--vrts-admin-theme-color);
font-weight: 600;
&::after {
opacity: 1;
}
}
@media (max-width: 480px) {
padding: 16px 0;
}
&:hover,
&:focus {
color: var(--vrts-admin-theme-color);
}
&:focus {
box-shadow: inset 0 0 0 2px var(--vrts-admin-theme-color);
}
// required to match the color scheme
&:not(:hover):not(:focus) {
color: $gray-text;
}
}
&-button {
align-self: center;
border: 1px solid var(--vrts-admin-theme-color);
border-radius: 50px;
padding: 8px 14px;
margin-left: 20px;
color: var(--vrts-admin-theme-color);
text-decoration: none;
&::after {
content: attr(title);
position: relative;
display: block;
font-weight: 700;
height: 1px;
color: transparent;
overflow: hidden;
visibility: hidden;
}
[aria-current="true"] & {
border: 1px solid var(--vrts-admin-theme-color);
background: rgba(var(--vrts-admin-theme-color--rgb), 0.1);
font-weight: initial;
}
@media (max-width: $break-medium) {
margin-left: 0;
}
&:focus {
box-shadow: 0 0 0 2px var(--vrts-admin-theme-color);
}
}
}
.update-plugins {
background-color: $alert-red;
border-radius: 9px;
box-sizing: border-box;
color: $white;
display: inline-block;
font-size: 11px;
height: 18px;
line-height: 1.6;
margin: 0 0 -1px 0;
min-width: 18px;
padding: 0 5px;
text-align: center;
vertical-align: top;
&::after {
display: block;
content: attr(title);
font-weight: 700;
height: 1px;
color: transparent;
overflow: hidden;
visibility: hidden;
}
}
}
@@ -0,0 +1,32 @@
<?php
use Vrts\Core\Utilities\Url_Helpers;
?>
<nav class="vrts-admin-header">
<a class="vrts-admin-header__logo" href="<?php echo esc_url( Url_Helpers::get_page_url( 'tests' ) ); ?>">
<span class="vrts-admin-header__logo-icon"><?php vrts()->logo(); ?></span>
<h2 class="vrts-admin-header__logo-text"><?php echo esc_html( $data['plugin_name'] ); ?></h2>
</a>
<ul class="vrts-admin-header__navigation">
<?php
$allowed_html_into_text = [
'span' => [
'class' => [],
'title' => [],
],
];
$allowed_html_into_title = [];
foreach ( $data['menu_items'] as $menu_item ) {
printf(
'<li class="vrts-admin-header__navigation-item" aria-current="%1$s"><a class="vrts-admin-header__navigation-%5$s" href="%2$s" title="%3$s">%4$s</a></li>',
! empty( $menu_item['is_active'] ) ? 'true' : 'false',
esc_url( $menu_item['url'] ),
wp_kses( $menu_item['text'], $allowed_html_into_title ),
wp_kses( $menu_item['text'], $allowed_html_into_text ),
esc_attr( strpos( $menu_item['url'], 'vrts-upgrade' ) !== false ? 'button' : 'link' )
);
}
?>
</ul>
</nav>
@@ -0,0 +1,27 @@
@import "@wordpress/base-styles/colors.native"; // stylelint-disable-line scss/at-import-partial-extension
.vrts-notice {
padding-bottom: 0.75em;
p,
form {
display: inline;
padding-right: 0;
padding-left: 0;
}
.admin-notice-button {
border: 0;
margin-bottom: 2px;
background: transparent;
padding: 0;
min-height: 19px;
line-height: 1;
border-radius: 0;
text-decoration: underline;
&:hover {
background: transparent;
}
}
}
@@ -0,0 +1,63 @@
<?php
switch ( $data['view'] ) {
case 'connection_failed':
$template = __DIR__ . '/views/admin-notification-connection-failed.php';
break;
case 'get_started':
$template = __DIR__ . '/views/admin-notification-get-started.php';
break;
case 'new_test_added':
$template = __DIR__ . '/views/admin-notification-new-test-added.php';
break;
case 'new_test_failed':
$template = __DIR__ . '/views/admin-notification-new-test-failed.php';
break;
case 'new_tests_added':
$template = __DIR__ . '/views/admin-notification-new-tests-added.php';
break;
case 'plugin_activated':
$template = __DIR__ . '/views/admin-notification-plugin-activated.php';
break;
case 'settings_saved':
$template = __DIR__ . '/views/admin-notification-settings-saved.php';
break;
case 'license_added':
$template = __DIR__ . '/views/admin-notification-license-added.php';
break;
case 'license_not_added':
$template = __DIR__ . '/views/admin-notification-license-not-added.php';
break;
case 'test_disabled':
$template = __DIR__ . '/views/admin-notification-test-disabled.php';
break;
case 'test_started':
$template = __DIR__ . '/views/admin-notification-test-started.php';
break;
case 'test_failed':
$template = __DIR__ . '/views/admin-notification-test-failed.php';
break;
case 'unlock_more_tests':
$template = __DIR__ . '/views/admin-notification-unlock-more-tests.php';
break;
default:
$template = '';
break;
}//end switch
if ( file_exists( $template ) ) {
include $template;
}
@@ -0,0 +1,26 @@
/* global jQuery, ajaxurl */
import 'scripts/relative-time-element';
jQuery( document ).ready( function ( $ ) {
$( document ).on( 'click', '.vrts-notice .notice-dismiss', ( event ) => {
if ( ajaxurl ) {
const nonce =
event.target.parentElement.querySelector( '#_wpnonce' ).value;
const view = event.target.parentElement.dataset.view;
const data = {
action: 'vrts_admin_notice_dismiss',
url: ajaxurl,
security: nonce,
view,
};
$.ajax( {
type: 'POST',
url: ajaxurl,
data,
} );
}
} );
} );
@@ -0,0 +1,8 @@
<div class="vrts-notice notice notice-error" data-view="<?php echo esc_attr( $data['view'] ); ?>">
<h3><?php esc_html_e( 'Connection failed', 'visual-regression-tests' ); ?></h3>
<p><?php esc_html_e( 'Something went wrong while trying to connect to the external service.', 'visual-regression-tests' ); ?></p>
<form id="form-retry-connection" method="post">
<?php wp_nonce_field( 'vrts_retry_connection_nonce' ); ?>
<?php submit_button( esc_attr__( 'Retry connection', 'visual-regression-tests' ), 'admin-notice-button', 'submit_retry_connection', false ); ?>
</form>
</div>
@@ -0,0 +1,5 @@
<div class="vrts-notice notice notice-info is-dismissible" data-view="<?php echo esc_attr( $data['view'] ); ?>">
<?php wp_nonce_field( 'vrts_admin_notice_nonce' ); ?>
<h3><?php esc_html_e( 'Lets get started!', 'visual-regression-tests' ); ?></h3>
<p><?php esc_html_e( 'Click “Add New“ to create a Visual Regression Test for your Website and find issues before others do.', 'visual-regression-tests' ); ?></p>
</div>
@@ -0,0 +1,5 @@
<div class="vrts-notice notice notice-success" data-view="<?php echo esc_attr( $data['view'] ); ?>">
<?php wp_nonce_field( 'vrts_admin_notice_nonce' ); ?>
<h3><?php esc_html_e( 'License activated!', 'visual-regression-tests' ); ?></h3>
<p><?php esc_html_e( 'Enjoy your upgraded features for seamless visual testing.', 'visual-regression-tests' ); ?></p>
</div>
@@ -0,0 +1,14 @@
<div class="vrts-notice notice notice-error" data-view="<?php echo esc_attr( $data['view'] ); ?>">
<?php wp_nonce_field( 'vrts_admin_notice_nonce' ); ?>
<h3><?php esc_html_e( 'Invalid License Key', 'visual-regression-tests' ); ?></h3>
<p>
<?php
printf(
/* translators: %1$s, %2$s: link wrapper. */
esc_html__( 'We could not verify the license key you entered. Please check for typos and try again. If the issue persists, %1$scontact us%2$s for assistance. Were here to help!', 'visual-regression-tests' ),
'<a href="https://vrts.app/contact/" target="_blank">',
'</a>'
);
?>
</p>
</div>
@@ -0,0 +1,13 @@
<div class="vrts-notice notice notice-success" data-view="<?php echo esc_attr( $data['view'] ); ?>">
<?php wp_nonce_field( 'vrts_admin_notice_nonce' ); ?>
<h3><?php esc_html_e( 'Test added successfully', 'visual-regression-tests' ); ?></h3>
<p>
<?php
printf(
/* translators: %s: the title of the page. */
esc_html__( 'Tests will be run for the following page: %s', 'visual-regression-tests' ),
esc_html( $data['page_title'] )
);
?>
</p>
</div>
@@ -0,0 +1,18 @@
<?php
use Vrts\Core\Utilities\Url_Helpers;
?>
<div class="vrts-notice notice notice-error is-dismissable" data-view="<?php echo esc_attr( $data['view'] ); ?>">
<h3><?php esc_html_e( 'Ready for an Upgrade?', 'visual-regression-tests' ); ?></h3>
<p>
<?php
printf(
'%1$s <a href="%2$s" title="%3$s">%3$s</a>',
esc_html__( 'Looks like you need a bigger plan to add more tests.', 'visual-regression-tests' ),
esc_url( Url_Helpers::get_page_url( 'upgrade' ) ),
esc_html__( 'Upgrade here!', 'visual-regression-tests' )
);
?>
</p>
</div>
@@ -0,0 +1,13 @@
<div class="vrts-notice notice notice-success" data-view="<?php echo esc_attr( $data['view'] ); ?>">
<?php wp_nonce_field( 'vrts_admin_notice_nonce' ); ?>
<h3><?php esc_html_e( 'Tests added successfully', 'visual-regression-tests' ); ?></h3>
<p>
<?php
printf(
/* translators: %s: the title of the page. */
esc_html__( 'Tests will be run for the following pages: %s', 'visual-regression-tests' ),
esc_html( $data['page_titles'] )
);
?>
</p>
</div>
@@ -0,0 +1,20 @@
<?php
use Vrts\Core\Utilities\Url_Helpers;
?>
<div class="notice updated is-dismissible" data-view="<?php echo esc_attr( $data['view'] ); ?>">
<p>
<strong><?php esc_html_e( 'VRTs Plugin successfully activated!', 'visual-regression-tests' ); ?></strong>
<?php
printf(
/* translators: %1$s, %2$s and %3$s, %4$s: link wrapper. */
esc_html__( 'Start to %1$sconfigure tests%2$s, or check the customization options in the %3$splugin settings%4$s.', 'visual-regression-tests' ),
'<a href="' . esc_url( Url_Helpers::get_page_url( 'tests' ) ) . '">',
'</a>',
'<a href="' . esc_url( Url_Helpers::get_page_url( 'settings' ) ) . '">',
'</a>'
);
?>
</p>
</div>
@@ -0,0 +1,5 @@
<div class="vrts-notice notice notice-success" data-view="<?php echo esc_attr( $data['view'] ); ?>">
<?php wp_nonce_field( 'vrts_admin_notice_nonce' ); ?>
<h3><?php esc_html_e( 'Settings saved', 'visual-regression-tests' ); ?></h3>
<p><?php esc_html_e( 'Changes have been saved successfully.', 'visual-regression-tests' ); ?></p>
</div>
@@ -0,0 +1,18 @@
<div class="vrts-notice notice notice-error" data-view="<?php echo esc_attr( $data['view'] ); ?>">
<?php wp_nonce_field( 'vrts_admin_notice_nonce' ); ?>
<h3><?php esc_html_e( 'Test disabled successfully', 'visual-regression-tests' ); ?></h3>
<p>
<?php
printf(
/* translators: %s: the title of the page. */
esc_html__( 'No more tests will be run for the following page: %s', 'visual-regression-tests' ),
esc_html( $data['page_title'] )
);
?>
</p>
<form id="form-undo-test" method="post">
<input type="hidden" name="post_id" value="<?php echo esc_attr( $data['post_id'] ); ?>">
<?php wp_nonce_field( 'vrts_page_tests_nonce' ); ?>
<?php submit_button( esc_attr__( 'Undo', 'visual-regression-tests' ), 'admin-notice-button', 'submit_add_new_test', false ); ?>
</form>
</div>
@@ -0,0 +1,5 @@
<div class="vrts-notice notice notice-error" data-view="<?php echo esc_attr( $data['view'] ); ?>">
<?php wp_nonce_field( 'vrts_admin_notice_nonce' ); ?>
<h3><?php esc_html_e( 'Test couldnt be started', 'visual-regression-tests' ); ?></h3>
<p><?php esc_html_e( 'A Test is already in progress. Please wait for the current Test to finish, then try again.', 'visual-regression-tests' ); ?></p>
</div>
@@ -0,0 +1,6 @@
<div class="vrts-notice notice notice-info" data-view="<?php echo esc_attr( $data['view'] ); ?>">
<?php wp_nonce_field( 'vrts_admin_notice_nonce' ); ?>
<h3><?php esc_html_e( 'Testing has started 🚀', 'visual-regression-tests' ); ?></h3>
<p><?php esc_html_e( 'New screenshots are being taken and compared with the previous version.', 'visual-regression-tests' ); ?></p><br>
<p><?php esc_html_e( 'This may take a moment. Youll receive an email if any visual changes are detected.', 'visual-regression-tests' ); ?></p>
</div>
@@ -0,0 +1,27 @@
<?php
use Vrts\Core\Utilities\Url_Helpers;
?>
<div class="vrts-notice notice notice-info" data-view="<?php echo esc_attr( $data['view'] ); ?>">
<h3><?php esc_html_e( 'Unlock more tests', 'visual-regression-tests' ); ?></h3>
<p>
<?php
printf(
'%s %s',
sprintf(
/* translators: %1$s, %2$s: number of tests. */
esc_html__( 'Good work! You have added %1$s of %2$s available tests.', 'visual-regression-tests' ),
intval( $data['total_tests'] ) - intval( $data['remaining_tests'] ),
intval( $data['total_tests'] )
),
sprintf(
/* translators: %1$s, %2$s: link wrapper. */
esc_html__( 'Upgrade %1$shere%2$s to add more tests to your website!', 'visual-regression-tests' ),
'<a href="' . esc_url( Url_Helpers::get_page_url( 'upgrade' ) ) . '">',
'</a>'
)
)
?>
</p>
</div>
@@ -0,0 +1,129 @@
.vrts-alert-actions {
display: block;
position: relative;
&__trigger {
border: 0;
background: transparent;
color: inherit;
cursor: pointer;
line-height: 1;
padding: 0.5rem;
width: 42px;
height: 42px;
&::before {
position: absolute;
top: 5px;
left: 5px;
right: 5px;
bottom: 5px;
content: "";
background: #f6f7f7;
border-radius: 2px;
opacity: 0;
z-index: -1;
transition: opacity 0.2s;
}
&:hover,
&:focus-visible,
&[aria-expanded="true"] {
&::before {
opacity: 1;
}
}
}
&__dropdown {
position: absolute;
top: calc(100% - 5px);
right: 0;
background: #fff;
border: 1px solid #c3c4c7;
box-shadow: 0 3px 28px 0 rgba(0, 0, 0, 0.15);
opacity: 0;
visibility: hidden;
min-width: 200px;
transform: translateY(-4px);
will-change: transform, opacity, visibility;
transition: all 0.15s;
&[aria-hidden="false"] {
opacity: 1;
visibility: visible;
transform: none;
}
}
&__dropdown-action {
cursor: pointer;
display: flex;
align-items: center;
gap: 0.375rem;
background: transparent;
border: 0;
white-space: nowrap;
padding: 12px 15px;
width: 100%;
text-decoration: none;
color: inherit;
transition: color 0.2s;
&:hover,
&:focus-visible {
color: var(--vrts-admin-theme-color);
}
&:not(:last-child) {
border-bottom: 1px solid #ccc;
}
}
&__modal {
.vrts-modal__content {
max-width: 450px;
}
.vrts-modal__content-inner {
padding-top: 15px;
}
&-action {
margin-top: 10px;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
&-action-success {
color: #00a32a;
font-size: 0.75rem;
margin-left: 0.5rem;
display: none;
&.is-active {
display: block;
}
}
.spinner {
margin: 0;
display: none;
&.is-active {
display: block;
}
}
textarea {
border-color: #ccc;
}
.description {
color: #757575;
}
}
}
@@ -0,0 +1,58 @@
<?php
use Vrts\Core\Utilities\Url_Helpers;
?>
<vrts-alert-actions class="vrts-alert-actions">
<button type="button" data-vrts-dropdown-open class="vrts-alert-actions__trigger" aria-expanded="false" aria-controls="vrts-alert-actions-dropdown">
<?php vrts()->icon( 'more-horizontal' ); ?>
</button>
<div id="vrts-alert-actions-dropdown" class="vrts-alert-actions__dropdown" aria-hidden="true">
<button data-vrts-loading="false" data-vrts-action-state="<?php echo esc_attr( $data['alert']->is_false_positive ? 'secondary' : 'primary' ); ?>" data-vrts-alert-id="<?php echo esc_attr( $data['alert']->id ); ?>" data-vrts-alert-action="false-positive" class="vrts-alert-actions__dropdown-action vrts-action-button">
<span class="vrts-action-button__icons">
<span class="vrts-action-button__icon" data-vrts-action-state-primary><?php vrts()->icon( 'flag-outline' ); ?></span>
<span class="vrts-action-button__icon" data-vrts-action-state-secondary><?php vrts()->icon( 'flag' ); ?></span>
<span class="vrts-action-button__spinner"><?php vrts()->icon( 'spinner' ); ?></span>
</span>
<span class="vrts-action-button__info" data-vrts-action-state-primary><?php esc_html_e( 'Flag as false positive', 'visual-regression-tests' ); ?></span>
<span class="vrts-action-button__info" data-vrts-action-state-secondary><?php esc_html_e( 'Unflag as false positive', 'visual-regression-tests' ); ?></span>
</button>
<button data-vrts-loading="false" data-vrts-action-state="<?php echo esc_attr( $data['alert']->alert_state ? 'secondary' : 'primary' ); ?>" data-vrts-alert-id="<?php echo esc_attr( $data['alert']->id ); ?>" data-vrts-alert-action="read-status" class="vrts-alert-actions__dropdown-action vrts-action-button">
<span class="vrts-action-button__icons">
<span class="vrts-action-button__icon" data-vrts-action-state-secondary><?php vrts()->icon( 'email-unread' ); ?></span>
<span class="vrts-action-button__icon" data-vrts-action-state-primary><?php vrts()->icon( 'email-read' ); ?></span>
<span class="vrts-action-button__spinner"><?php vrts()->icon( 'spinner' ); ?></span>
</span>
<span class="vrts-action-button__info" data-vrts-action-state-primary><?php esc_html_e( 'Mark as read', 'visual-regression-tests' ); ?></span>
<span class="vrts-action-button__info" data-vrts-action-state-secondary><?php esc_html_e( 'Mark as unread', 'visual-regression-tests' ); ?></span>
</button>
<button type="button" class="vrts-alert-actions__dropdown-action" data-a11y-dialog-show="vrts-modal-hide-elements">
<?php vrts()->icon( 'hidden' ); ?>
<?php esc_html_e( 'Hide elements', 'visual-regression-tests' ); ?>
</button>
</div>
<vrts-modal id="vrts-modal-hide-elements" class="vrts-modal vrts-alert-actions__modal" aria-hidden="true">
<div class="vrts-modal__overlay" data-a11y-dialog-hide></div>
<div class="vrts-modal__content" role="document">
<button type="button" class="vrts-modal__close" data-a11y-dialog-hide aria-label="<?php esc_attr_e( 'Close', 'visual-regression-tests' ); ?>"></button>
<h2 class="vrts-modal__title">
<?php vrts()->icon( 'hidden' ); ?>
<?php esc_html_e( 'Hide elements', 'visual-regression-tests' ); ?>
</h2>
<div class="vrts-modal__content-inner">
<form data-vrts-hide-elements-form action="<?php echo esc_url( Url_Helpers::get_page_url( 'runs' ) ); ?>" method="put">
<input type="hidden" name="post_id" value="<?php echo esc_attr( $data['alert']->post_id ); ?>">
<input type="hidden" name="test_id" value="<?php echo esc_attr( $data['test_settings']['test_id'] ); ?>">
<textarea class="widefat" name="hide_css_selectors" placeholder="<?php esc_html_e( 'e.g.: .lottie, #ads', 'visual-regression-tests' ); ?>" rows="4"><?php echo esc_html( $data['test_settings']['hide_css_selectors'] ); ?></textarea>
<p class="description"><?php esc_html_e( 'Hide elements on snapshots to exclude them from comparisons.', 'visual-regression-tests' ); ?></p>
<div class="vrts-alert-actions__modal-action">
<button type="submit" class="button button-primary"><?php esc_html_e( 'Save Changes', 'visual-regression-tests' ); ?></button>
<span class="spinner"></span>
<span class="vrts-alert-actions__modal-action-success"><?php esc_html_e( 'Saved successfully.', 'visual-regression-tests' ); ?></span>
</div>
</form>
</div>
</div>
</vrts-modal>
</vrts-alert-actions>
@@ -0,0 +1,198 @@
import Dropdown from '../../assets/scripts/dropdown';
class VrtsAlertActions extends window.HTMLElement {
constructor() {
super();
this.dropdown = null;
this.setAsReadTimeout = null;
this.resolveElements();
this.bindFunctions();
this.bindEvents();
}
resolveElements() {
this.$actionButtons = this.querySelectorAll(
'[data-vrts-alert-action]'
);
this.$hideElementsForm = this.querySelector(
'[data-vrts-hide-elements-form]'
);
this.$hideElementsModal = document.getElementById(
'vrts-modal-hide-elements'
);
this.$spinner = this.querySelector( '.spinner' );
this.$success = this.querySelector(
'.vrts-alert-actions__modal-action-success'
);
}
bindFunctions() {
this.onActionClick = this.onActionClick.bind( this );
this.onHideElementsFormSubmit =
this.onHideElementsFormSubmit.bind( this );
this.onHideElementsModalClose =
this.onHideElementsModalClose.bind( this );
}
bindEvents() {
this.$actionButtons.forEach( ( item ) => {
item.addEventListener( 'click', this.onActionClick );
} );
this.$hideElementsForm.addEventListener(
'submit',
this.onHideElementsFormSubmit
);
this.$hideElementsModal.addEventListener(
'hide',
this.onHideElementsModalClose
);
}
connectedCallback() {
this.dropdown = Dropdown( this );
this.setAsReadOnView();
}
setAsReadOnView() {
this.$actionButtons.forEach( ( action ) => {
const isReadStatusAction =
action.getAttribute( 'data-vrts-alert-action' ) ===
'read-status';
const isUnread =
action.getAttribute( 'data-vrts-action-state' ) === 'primary';
if ( isReadStatusAction && isUnread ) {
this.setAsReadTimeout = setTimeout( () => {
action.click();
}, 1000 );
}
} );
}
onHideElementsFormSubmit( e ) {
e.preventDefault();
const $form = e.currentTarget;
const formData = new window.FormData( $form );
const postId = formData.get( 'post_id' );
this.$spinner.classList.add( 'is-active' );
this.$success.classList.remove( 'is-active' );
fetch( `${ window.vrts_admin_vars.rest_url }/tests/post/${ postId }`, {
method: 'PUT',
headers: {
'X-WP-Nonce': window.vrts_admin_vars.rest_nonce,
},
body: new URLSearchParams( formData ),
} )
.then( ( response ) => {
return response.json();
} )
.then( () => {
this.$spinner.classList.remove( 'is-active' );
this.$success.classList.add( 'is-active' );
} );
}
onHideElementsModalClose() {
this.$success.classList.remove( 'is-active' );
}
onActionClick( e ) {
const $el = e.currentTarget;
const isLoading = $el.getAttribute( 'data-vrts-loading' ) === 'true';
const state = $el.getAttribute( 'data-vrts-action-state' );
const isPrimary = state === 'primary';
if ( isLoading ) {
return;
}
const action = $el.getAttribute( 'data-vrts-alert-action' );
const id = $el.getAttribute( 'data-vrts-alert-id' );
this.handleAction( action, $el, id, isPrimary );
}
handleAction( action, $el, id, shouldSetAction ) {
const restEndpoint = `${ window.vrts_admin_vars.rest_url }/alerts/${ id }/${ action }`;
const method = shouldSetAction ? 'POST' : 'DELETE';
let loadingElapsedTime = 0;
let interval = null;
const timeout = setTimeout( () => {
$el.setAttribute( 'data-vrts-loading', 'true' );
const loadingStartTime = window.Date.now();
interval = setInterval( () => {
loadingElapsedTime = window.Date.now() - loadingStartTime;
}, 50 );
}, 200 );
fetch( restEndpoint, {
method,
headers: {
'X-WP-Nonce': window.vrts_admin_vars.rest_nonce,
},
} )
.then( ( response ) => {
return response.json();
} )
.then( () => {
const loadingTimeoutTime =
loadingElapsedTime > 0
? Math.abs( loadingElapsedTime - 400 )
: 0;
setTimeout( () => {
$el.setAttribute( 'data-vrts-loading', 'false' );
$el.setAttribute(
'data-vrts-action-state',
shouldSetAction ? 'secondary' : 'primary'
);
const $alert = document.getElementById(
`vrts-alert-${ id }`
);
if ( $alert ) {
if ( 'false-positive' === action ) {
$alert.setAttribute(
'data-vrts-false-positive',
shouldSetAction ? 'true' : 'false'
);
}
if ( 'read-status' === action ) {
$alert.setAttribute(
'data-vrts-state',
shouldSetAction ? 'read' : 'unread'
);
}
}
}, loadingTimeoutTime );
clearTimeout( timeout );
clearInterval( interval );
} );
}
disconnectedCallback() {
this.dropdown?.();
clearTimeout( this.setAsReadTimeout );
this.$actionButtons?.forEach( ( item ) => {
item.removeEventListener( 'click', this.onActionClick );
} );
this.$hideElementsForm?.removeEventListener(
'submit',
this.onHideElementsFormSubmit
);
this.$hideElementsModal?.removeEventListener(
'hide',
this.onHideElementsModalClose
);
}
}
window.customElements.define( 'vrts-alert-actions', VrtsAlertActions );
@@ -0,0 +1,300 @@
@import "@wordpress/base-styles/colors.native"; // stylelint-disable-line scss/at-import-partial-extension
@import "@wordpress/base-styles/breakpoints";
.vrts-comparisons {
--vrts-comparisons-slider-position: 50%;
display: block;
box-sizing: border-box;
border-top: 0;
margin-bottom: 0;
&__header {
background-color: $white;
z-index: 1;
gap: 1rem;
&:has(vrts-modal:not([aria-hidden="true"])) {
z-index: 999999;
}
@media (min-width: 850px) {
position: sticky;
top: calc(62px + var(--wp-admin--admin-bar--height));
[data-vrts-fullscreen="true"] & {
top: 62px;
}
}
}
&__title {
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
display: -webkit-box;
font-size: 13px;
font-weight: 500;
line-height: 1.4;
max-block-size: calc(13px * 1.4);
white-space: initial;
overflow: hidden;
text-overflow: ellipsis;
padding-left: 15px;
}
&__info {
display: flex;
align-items: center;
padding-right: 0.25rem;
z-index: 1;
> *:not(:last-child) {
position: relative;
&::after {
position: absolute;
right: -0.5px;
content: "";
display: block;
width: 1px;
height: 1rem;
background: $gray-10;
margin-left: 0.75rem;
}
}
}
&__difference {
display: flex;
align-items: center;
color: #b32d2e;
padding-right: 0.75rem;
white-space: nowrap;
}
&__expand-button {
background: transparent;
border: 0;
padding: 0;
cursor: pointer;
display: flex;
width: 42px;
height: 42px;
align-items: center;
justify-content: center;
@media (max-width: 667px) {
display: none;
}
&::before {
position: absolute;
top: 5px;
left: 5px;
right: 5px;
bottom: 5px;
content: "";
background: #f6f7f7;
border-radius: 2px;
opacity: 0;
z-index: -1;
transition: opacity 0.2s;
}
&:hover,
&:focus-visible {
&::before {
opacity: 1;
}
}
svg {
height: 14px;
width: auto;
&:last-child {
display: none;
}
}
[data-vrts-fullscreen="true"] & {
svg:first-child {
display: none;
}
svg:last-child {
display: block;
}
}
}
&__content {
display: grid;
grid-template-columns: repeat(3, minmax(0, max-content));
justify-content: center;
gap: 0.25rem;
padding: 15px;
@container (max-width: 600px) {
gap: 0;
grid-template-columns: repeat(2, minmax(0, max-content));
}
&-container {
container-type: inline-size;
}
}
&__figure {
margin: 0;
position: relative;
border: solid 1px #{$gray-lighten-20};
min-height: 100px;
&-image {
box-sizing: border-box;
display: block;
max-width: 100%;
height: auto;
margin: 0 auto;
}
@container (max-width: 600px) {
&[data-vrts-comparisons-slot="comparison"] {
display: none;
}
}
}
&__diff-inidicator {
box-sizing: border-box;
background: #f0f0f1;
border-block: 1px solid transparent;
height: 100%;
image-rendering: pixelated;
width: 4px;
@container (max-width: 600px) {
margin-left: -9.5px;
}
}
&__slider {
--vrts-comparisons-slider-height: 100%;
position: relative;
display: grid;
align-items: start;
border: solid 1px #{$gray-lighten-20};
overflow: clip;
.vrts-comparisons__figure {
grid-column: 1 / 1;
grid-row: 1 / 1;
margin: 0;
border: 0;
height: 100%;
width: 100%;
}
[data-vrts-comparisons-slot="base"] {
clip-path: inset(0 calc(100% - var(--vrts-comparisons-slider-position)) 0 0);
}
[data-vrts-comparisons-slot="target"] {
clip-path: inset(0 0 0 var(--vrts-comparisons-slider-position));
}
&:has(input:focus-visible) {
.vrts-comparisons__slider-handle svg {
outline: 2px solid var(--vrts-admin-theme-color);
outline-offset: 2px;
}
}
}
&__slider-handle {
--vrts-comparisons-offset: calc(62px + 42px);
--vrts-comparisons-handle-size: 32px;
--vrts-comparisons-handle-offest: 20px;
--vrts-comparisons-slider-centered: calc(100% / var(--vrts-comparisons-slider-aspect-ratio) / 2 - var(--vrts-comparisons-handle-size) / 2);
--vrts-comparisons-viewport-centered: calc(((100vh - var(--wp-admin--admin-bar--height) - 56px - 15px - var(--vrts-comparisons-offset)) / 2 - var(--vrts-comparisons-handle-size) / 2) + 15%);
box-sizing: border-box;
position: sticky;
top: calc((var(--vrts-comparisons-handle-size) / 2) + 10%);
width: 100%;
height: calc(var(--vrts-comparisons-handle-size) + 10px);
display: flex;
align-items: flex-start;
justify-content: center;
grid-column: 1 / 1;
grid-row: 1 / 1;
margin-block: var(--vrts-comparisons-handle-offest);
margin-block-start: max(var(--vrts-comparisons-handle-offest), min(var(--vrts-comparisons-slider-centered), var(--vrts-comparisons-viewport-centered)));
@media (min-width: 851px) {
top: calc(var(--wp-admin--admin-bar--height) + var(--vrts-comparisons-offset) + (var(--vrts-comparisons-handle-size) / 2) + 10%);
}
[data-vrts-fullscreen="true"] & {
top: calc(var(--vrts-comparisons-offset) + (var(--vrts-comparisons-handle-size) / 2) + 10%);
}
svg { // stylelint-disable-line no-descending-specificity
box-sizing: border-box;
position: absolute;
top: 0;
left: calc(var(--vrts-comparisons-slider-position) - (var(--vrts-comparisons-handle-size) / 2));
width: var(--vrts-comparisons-handle-size);
height: var(--vrts-comparisons-handle-size);
border-radius: 50%;
background: #fff;
border: 1px solid var(--vrts-admin-theme-color);
color: var(--vrts-admin-theme-color);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
}
&__slider-divider {
position: absolute;
top: 0;
left: calc(var(--vrts-comparisons-slider-position) - 0.5px);
width: 1px;
height: 100%;
background: var(--vrts-admin-theme-color);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
&__slider-divider-clone {
position: absolute;
top: 0;
left: calc(var(--vrts-comparisons-slider-position) - 0.5px);
width: 0;
height: 100%;
border-left: 1px dashed #c3c4c7;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
opacity: 0.4;
}
&__slider-control {
appearance: none;
position: absolute;
inset: 0;
opacity: 0;
&::-webkit-slider-runnable-track {
height: 100%;
}
&::-webkit-slider-thumb {
appearance: none;
height: 100%;
width: 56px;
}
}
}
@@ -0,0 +1,46 @@
<?php
use Vrts\Core\Utilities\Image_Helpers;
?>
<vrts-comparisons class="vrts-comparisons postbox">
<div class="vrts-comparisons__header postbox-header">
<div class="vrts-comparisons__title"><?php echo esc_html( get_the_title( $data['alert']->post_id ) ); ?></div>
<div class="vrts-comparisons__info">
<div class="vrts-comparisons__difference">
<?php /* translators: %s: the count of pixels with a visual difference. */ ?>
<?php echo esc_html( sprintf( __( '%spx Difference', 'visual-regression-tests' ), esc_html( number_format_i18n( ceil( $data['alert']->differences ) ) ) ) ); ?>
</div>
<button type="button" title="<?php esc_html_e( 'Expand', 'visual-regression-tests' ); ?>" class="vrts-comparisons__expand-button" data-vrts-fullscreen-open>
<?php vrts()->icon( 'expand' ); ?>
<?php vrts()->icon( 'compress' ); ?>
</button>
<?php vrts()->component( 'alert-actions', $data ); ?>
</div>
</div>
<div class="vrts-comparisons__content-container">
<div class="vrts-comparisons__content">
<figure class="vrts-comparisons__figure" data-vrts-comparisons-slot="comparison">
<img class="vrts-comparisons__figure-image" <?php echo wp_kses_post( Image_Helpers::alert_image_hwstring( $data['alert'] ) ); ?> crossorigin="anonymous" src="<?php echo esc_url( Image_Helpers::get_screenshot_url( $data['alert'], 'comparison' ) ); ?>" alt="<?php esc_attr_e( 'Difference', 'visual-regression-tests' ); ?>" />
<span class="vrts-comparisons__slider-divider-clone"></span>
</figure>
<canvas class="vrts-comparisons__diff-inidicator" data-vrts-comparisons-diff-inidicator></canvas>
<div class="vrts-comparisons__slider" style="--vrts-comparisons-slider-aspect-ratio: <?php echo esc_attr( Image_Helpers::alert_image_aspect_ratio( $data['alert'] ) ); ?>">
<figure class="vrts-comparisons__figure" data-vrts-comparisons-slot="base">
<img class="vrts-comparisons__figure-image" <?php echo wp_kses_post( Image_Helpers::alert_image_hwstring( $data['alert'] ) ); ?> crossorigin="anonymous" src="<?php echo esc_url( Image_Helpers::get_screenshot_url( $data['alert'], 'base' ) ); ?>" alt="<?php esc_attr_e( 'Snapshot', 'visual-regression-tests' ); ?>" />
</figure>
<figure class="vrts-comparisons__figure" data-vrts-comparisons-slot="target">
<img class="vrts-comparisons__figure-image" <?php echo wp_kses_post( Image_Helpers::alert_image_hwstring( $data['alert'] ) ); ?> crossorigin="anonymous" src="<?php echo esc_url( Image_Helpers::get_screenshot_url( $data['alert'], 'target' ) ); ?>" alt="<?php esc_attr_e( 'Screenshot', 'visual-regression-tests' ); ?>" />
</figure>
<span class="vrts-comparisons__slider-divider"></span>
<div class="vrts-comparisons__slider-handle">
<?php vrts()->icon( 'grip-dots' ); ?>
</div>
<input type="range" step="0.078125" class="vrts-comparisons__slider-control" data-vrts-comparisons-slider-control>
</div>
</div>
</div>
<span class="vrts-gradient-loader"></span>
</vrts-comparisons>
@@ -0,0 +1,163 @@
class VrtsComparisons extends window.HTMLElement {
constructor() {
super();
this.resolveElements();
this.bindFunctions();
this.bindEvents();
this.init();
}
resolveElements() {
this.$content = document.querySelector( '[data-vrts-fullscreen]' );
this.$fullscreen = this.querySelector( '[data-vrts-fullscreen-open]' );
this.$control = this.querySelector(
'[data-vrts-comparisons-slider-control]'
);
this.$diffIndicator = this.querySelector(
'[data-vrts-comparisons-diff-inidicator]'
);
this.$comparison = this.querySelector(
'[data-vrts-comparisons-slot="comparison"] img'
);
}
bindFunctions() {
this.onFullscreenToggle = this.onFullscreenToggle.bind( this );
this.onControlChange = this.onControlChange.bind( this );
this.onFullScreenChange = this.onFullScreenChange.bind( this );
this.onLoadComparison = this.onLoadComparison.bind( this );
}
bindEvents() {
this.$fullscreen.addEventListener( 'click', this.onFullscreenToggle );
this.$control.addEventListener( 'input', this.onControlChange );
document.addEventListener(
'fullscreenchange',
this.onFullScreenChange
);
}
init() {
this.worker = new window.Worker(
new URL( 'worker.js', import.meta.url )
);
this.worker.onmessage = this.onWorkerMessage.bind( this );
if ( this.$comparison.complete ) {
this.onLoadComparison();
} else {
this.$comparison.onload = this.onLoadComparison;
}
}
onLoadComparison() {
const imageData = this.createOffscreenCanvarImageData(
this.$comparison
);
this.worker.postMessage( {
action: 'analyzeImage',
imageData,
} );
}
createOffscreenCanvarImageData( image ) {
const canvas = new window.OffscreenCanvas(
image.naturalWidth,
image.naturalHeight
);
const ctx = canvas.getContext( '2d' );
ctx.drawImage( image, 0, 0 );
return ctx.getImageData( 0, 0, canvas.width, canvas.height );
}
requestFullscreen( element ) {
if ( element.requestFullscreen ) {
element.requestFullscreen();
} else if ( element.webkitRequestFullscreen ) {
element.webkitRequestFullscreen();
} else if ( element.msRequestFullscreen ) {
element.msRequestFullscreen();
}
}
exitFullscreen() {
if ( document.exitFullscreen ) {
document.exitFullscreen();
} else if ( document.webkitExitFullscreen ) {
document.webkitExitFullscreen();
} else if ( document.msExitFullscreen ) {
document.msExitFullscreen();
}
}
getFullscreenElement() {
return (
document.fullscreenElement ||
document.webkitFullscreenElement ||
document.msFullscreenElement
);
}
onFullscreenToggle( e ) {
e.preventDefault();
if ( this.getFullscreenElement() === this.$content ) {
this.exitFullscreen();
} else {
this.requestFullscreen( this.$content );
}
}
onFullScreenChange() {
this.$content.setAttribute(
'data-vrts-fullscreen',
this.getFullscreenElement() === this.$content
);
}
onControlChange( e ) {
this.style.setProperty(
'--vrts-comparisons-slider-position',
`${ e.target.value }%`
);
}
onWorkerMessage( e ) {
if ( e.data?.action === 'analyzedImage' ) {
this.highlightPixels( e.data.coloredPixels );
}
}
highlightPixels( pixels ) {
const ctx = this.$diffIndicator.getContext( '2d' );
this.$diffIndicator.width = this.$comparison.naturalWidth;
this.$diffIndicator.height = this.$comparison.naturalHeight;
ctx.clearRect(
0,
0,
this.$comparison.naturalWidth,
this.$comparison.naturalHeight
);
ctx.fillStyle = '#cc1818';
pixels.forEach( ( y ) => {
ctx.fillRect( 0, y - 2, ctx.canvas.width, 3 );
} );
}
connectedCallback() {}
disconnectedCallback() {
this.$fullscreen?.removeEventListener(
'click',
this.onFullscreenToggle
);
this.$control?.removeEventListener( 'input', this.onControlChange );
document.removeEventListener(
'fullscreenchange',
this.onFullScreenChange
);
this.worker?.terminate();
this.worker = null;
}
}
window.customElements.define( 'vrts-comparisons', VrtsComparisons );
@@ -0,0 +1,30 @@
/* global self */
function getColoredPixelsWithLimit( imageData, xLimit, direction = 'left' ) {
const coloredPixels = new Set();
for ( let i = 0; i < imageData.data.length; i += 4 ) {
const r = imageData.data[ i ];
const g = imageData.data[ i + 1 ];
const b = imageData.data[ i + 2 ];
const a = imageData.data[ i + 3 ];
if ( r === 255 && g === 0 && b === 0 && a === 255 ) {
const x = ( i / 4 ) % imageData.width;
if ( direction === 'left' ? x < xLimit : x > xLimit ) {
// push x and y coordinates
coloredPixels.add( Math.floor( i / 4 / imageData.width ) );
}
}
}
return Array.from( coloredPixels );
}
self.onmessage = function ( e ) {
if ( e.data?.action === 'analyzeImage' ) {
const { imageData } = e.data;
const coloredPixels = getColoredPixelsWithLimit(
imageData,
0,
'right'
);
self.postMessage( { action: 'analyzedImage', coloredPixels } );
}
};
@@ -0,0 +1,273 @@
<?php
use Vrts\Core\Utilities\Date_Time_Helpers;
use Vrts\Core\Utilities\String_Helpers;
use Vrts\Core\Utilities\Url_Helpers;
use Vrts\Models\Test_Run;
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html lang="en" dir="ltr" xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Title</title>
<meta charset="UTF-8">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="x-apple-disable-message-reformatting" />
<meta content="telephone=no" name="format-detection">
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<!--<![endif]-->
<!--[if gte mso 9]>
<xml>
<o:OfficeDocumentSettings xmlns:o="urn:schemas-microsoft-com:office:office">
<o:AllowPNG />
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
<style>
table,
tr,
td {
border-collapse: separate;
padding: 0;
}
</style>
<!--[if mso]>
<style type="text/css">
v\:* {
behavior: url(#default#VML);
display: inline-block;
}
o\:* {
behavior: url(#default#VML);
display: inline-block;
}
w\:* {
behavior: url(#default#VML);
display: inline-block;
}
.ExternalClass {
width: 100%;
}
table {
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
-ms-interpolation-mode: bicubic;
}
.ReadMsgBody {
width: 100%;
}
a {
background: transparent !important;
background-color: transparent !important;
}
li {
text-align: -webkit-match-parent;
display: list-item;
text-indent: -1em;
}
ul,
ol {
margin-left: 1em !important;
}
p {
text-indent: 0;
}
</style>
<![endif]-->
</head>
<body style="width:100%;height:100%;padding:0;Margin:0">
<table border="0" cellspacing="0" width="100%" bgcolor="#ffffff" style="background-color: #ffffff; font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, sans-serif, Tahoma, system-ui;">
<tr>
<td style="padding: 20px;">
<p style="font-size: 14px; line-height: 16px; color: #2C3338; display: block; mso-line-height-rule: exactly"><?php esc_html_e( 'Howdy 👋', 'visual-regression-tests' ); ?></p>
<p style="font-size: 14px; line-height: 16px; color: #2C3338; display: block; mso-line-height-rule: exactly; margin: 0"><?php esc_html_e( "We've detected visual changes on your website.", 'visual-regression-tests' ); ?></p>
</td>
</tr>
</table>
<table border="0" cellspacing="0" bgcolor="#ffffff" style="background-color: #ffffff; font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, sans-serif, Tahoma, system-ui;">
<tr>
<td width="1"></td>
<td width="280" style="padding: 0 20px;">
<table cellpadding="0" cellspacing="0" border="0" role="presentation" style="width: 100%; border-spacing: 0; background-color: #F6F7FB;">
<tr><td style="padding: 0 15px;">
<table cellpadding="0" cellspacing="0" border="0" role="presentation" style="width: 100%; border-spacing: 0; font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, sans-serif, Tahoma, system-ui;">
<tr>
<td style="border-bottom: 1px dashed #c3c4c7; padding: 12px 0">
<center>
<table cellpadding="0" cellspacing="0" border="0" style="border-spacing: 0; font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, sans-serif, Tahoma, system-ui;">
<tr>
<td width="22" style="padding-right: 8px;">
<img width="22" height="22" src="<?php echo esc_url( vrts()->get_plugin_url( 'assets/images/vrts-logo.png' ) ); ?>" alt="Logo">
</td>
<td width="100">
<p style="font-size: 16px; line-height: 20px; color: #2C3338; display: block; margin: 0; mso-line-height-rule: exactly;"><strong><?php esc_html_e( 'VRTs Plugin', 'visual-regression-tests' ); ?></strong></p>
</td>
</tr>
</table>
</center>
<table cellpadding="0" cellspacing="0" border="0" style="width: 100%; border-spacing: 0; padding-top: 10px; font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, sans-serif, Tahoma, system-ui;">
<tr>
<td style="text-align: center">
<p style="font-size: 14px; line-height: 20px; color: #2C3338; display: block; margin: 0; mso-line-height-rule: exactly"><?php esc_html_e( 'Test Receipt', 'visual-regression-tests' ); ?></p>
<a style="font-size: 11px; line-height: 16px; display: block; padding-top: 4px; mso-line-height-rule: exactly" href="<?php echo esc_url( home_url( '/' ) ); ?>"><?php echo esc_html( home_url( '/' ) ); ?></a>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td style="border-bottom: 1px dashed #c3c4c7; padding: 12px 0; text-align: center;">
<p style="font-size: 11px; line-height: 16px; color: #2C3338; display: block; margin: 0; mso-line-height-rule: exactly">
<span style="padding-right: 10px">
<?php
// translators: %s: the run number.
printf( esc_html__( 'Run #%s', 'visual-regression-tests' ), esc_html( $data['run']->id ) );
?>
</span>
<?php echo wp_kses( Date_Time_Helpers::get_formatted_date_time( $data['run']->finished_at ), [ 'time' => [ 'datetime' => true ] ] ); ?>
</p>
</td>
</tr>
<tr>
<td style="border-bottom: 1px dashed #c3c4c7; padding: 12px 0;">
<table cellpadding="0" cellspacing="0" border="0" style="width: 100%; border-spacing: 0; font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, sans-serif, Tahoma, system-ui;">
<tr>
<th style="font-size: 12px; padding-bottom: 4px; line-height: 16px; color: #2C3338; mso-line-height-rule: exactly; text-align: left;">Pages</th>
<th style="font-size: 12px; padding-bottom: 4px; line-height: 16px; color: #2C3338; mso-line-height-rule: exactly; text-align: right;">Difference</th>
</tr>
<?php
foreach ( $data['tests'] as $test ) :
$alert = array_values( array_filter( $data['alerts'], static function( $alert ) use ( $test ) {
return $alert->post_id === $test['post_id'];
} ) );
$difference = $alert ? ceil( $alert[0]->differences ) : 0;
?>
<tr>
<td style="padding-top: 2px; padding-bottom: 2px; vertical-align: top; font-size: 12px; line-height: 16px; color: #2C3338; mso-line-height-rule: exactly; text-align: left;">
<?php if ( $test['permalink'] ) : ?>
<a style="color: #2C3338; text-decoration: none" href="<?php echo esc_url( $test['permalink'] ); ?>"><?php echo esc_html( String_Helpers::truncate( Url_Helpers::make_relative( $test['permalink'] ), 30 ) ); ?></a>
<?php else : ?>
N/A
<?php endif; ?>
</td>
<td style="padding-top: 2px; padding-bottom: 2px; vertical-align: top; font-size: 12px; line-height: 16px; color: #2C3338; mso-line-height-rule: exactly; text-align: right;">
<?php $difference_url = $difference ? Url_Helpers::get_alert_page( $alert[0]->id, $data['run']->id ) : Url_Helpers::get_alerts_page( $data['run']->id ); ?>
<a style="color: #2C3338; text-decoration: none" href="<?php echo esc_url( $difference_url ); ?>">
<?php
printf(
/* translators: %s. Test run receipt diff in pixels */
esc_html_x( '%spx', 'test run receipt difference', 'visual-regression-tests' ),
esc_html( number_format_i18n( $difference ) )
);
?>
</a>
</td>
</tr>
<?php endforeach; ?>
</table>
</td>
</tr>
<tr>
<td style="border-bottom: 1px dashed #c3c4c7; padding: 12px 0;">
<table cellpadding="0" cellspacing="0" border="0" style="width: 100%; border-spacing: 0; font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, sans-serif, Tahoma, system-ui;">
<tr>
<th style="font-size: 14px; padding-bottom: 4px; line-height: 16px; color: #2C3338; mso-line-height-rule: exactly; text-align: left;">Total</th>
<th style="font-size: 14px; padding-bottom: 4px; line-height: 16px; color: #2C3338; mso-line-height-rule: exactly; text-align: right;">
<?php
printf(
/* translators: %s. Number of tests */
esc_html( _n( '%s Test', '%s Tests', count( $data['tests'] ), 'visual-regression-tests' ) ), count( $data['tests'] )
); ?>
</th>
</tr>
<tr>
<td style="padding-top: 2px; padding-bottom: 2px; vertical-align: top; font-size: 12px; line-height: 16px; color: #4ab866; mso-line-height-rule: exactly; text-align: left;"><?php esc_html_e( 'Passed', 'visual-regression-tests' ); ?></td>
<td style="padding-top: 2px; padding-bottom: 2px; vertical-align: top; font-size: 12px; line-height: 16px; color: #4ab866; mso-line-height-rule: exactly; text-align: right;"><?php echo esc_html( count( $data['tests'] ) - count( $data['alerts'] ) ); ?></td>
</tr>
<tr>
<td style="padding-top: 2px; padding-bottom: 2px; vertical-align: top; font-size: 12px; line-height: 16px; color: #b32d2e; mso-line-height-rule: exactly; text-align: left;"><?php esc_html_e( 'Changes Detected', 'visual-regression-tests' ); ?></td>
<td style="padding-top: 2px; padding-bottom: 2px; vertical-align: top; font-size: 12px; line-height: 16px; color: #b32d2e; mso-line-height-rule: exactly; text-align: right;"><?php echo esc_html( count( $data['alerts'] ) ); ?></td>
</tr>
</table>
</td>
</tr>
<tr>
<td style="border-bottom: 1px dashed #c3c4c7; padding: 12px 0; font-size: 11px; line-height: 16px; color: #2C3338; mso-line-height-rule: exactly;">
<table>
<tr>
<td><?php esc_html_e( 'Trigger', 'visual-regression-tests' ); ?></td>
<td style="padding-left: 5px;">
<table cellpadding="0" cellspacing="0" border="0" role="presentation" bgcolor="#f0e8f8" width="100.00%" style="border-radius: 100px; background-color: <?php echo Test_Run::get_trigger_background_color( $data['run'] ); ?>; width: 100%; border-spacing: 0; border-collapse: separate; font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, sans-serif, Tahoma, system-ui;">
<tr>
<td align="center" valign="middle">
<p style="font-size: 12px; font-weight: 400; line-height: 16px; color: <?php echo Test_Run::get_trigger_text_color( $data['run'] ); ?>; mso-line-height-alt: 16px; margin: 0; padding: 4px 10px"><?php echo esc_html( Test_Run::get_trigger_title( $data['run'] ) ); ?></p>
</td>
</tr>
</table>
</td>
</tr>
</table>
<?php
$trigger_note = Test_Run::get_trigger_note( $data['run'] );
if ( ! empty( $trigger_note ) ) : ?>
<span style="color: #757575;">
<?php echo esc_html( $trigger_note ); ?>
</span>
<?php endif; ?>
</td>
</tr>
<tr>
<td style="padding-top: 12px; padding-bottom: 8px; text-align: center">
<p style="font-size: 13px; line-height: 16px; text-transform: uppercase; color: #2C3338; display: block; margin: 0; mso-line-height-rule: exactly"><strong><?php esc_html_e( 'Test Completed!', 'visual-regression-tests' ); ?></strong></p>
</td>
</tr>
<tr>
<td width="1" style="padding-bottom: 12px;">
<table cellpadding="0" cellspacing="0" border="0" role="presentation" bgcolor="#1954ed" width="100.00%" style="border-radius: 2px; background-color: #1954ed; width: 100%; border-spacing: 0; border-collapse: separate; font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, sans-serif, Tahoma, system-ui;">
<tr>
<td align="center" valign="middle" style="padding: 8px 24px;">
<a href="<?php echo Url_Helpers::get_test_run_page( $data['run'] ); ?>" style="font-size: 14px; font-weight: 600; color: white; margin: 0; padding: 0; line-height: 20px; mso-line-height-alt: 20px; text-decoration: none; display: block;"><?php esc_html_e( 'View Details', 'visual-regression-tests' ); ?></a>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td></tr>
</table>
</td>
<td></td>
</tr>
</table>
<table border="0" cellspacing="0" width="100%" bgcolor="#ffffff" style="background-color: #ffffff; font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, sans-serif, Tahoma, system-ui;">
<tr>
<td style="padding: 20px 20px;">
<p style="font-size: 14px; line-height: 16px; color: #2C3338; display: block; mso-line-height-rule: exactly; margin: 0"><?php esc_html_e( 'Your VRTs squad! 🚀', 'visual-regression-tests' ); ?></p>
<p style="font-size: 14px; line-height: 16px; color: #2C3338; display: block; mso-line-height-rule: exactly">
<?php
printf(
/* translators: %s: Home URL */
wp_kses_post( 'This alert was sent by the VRTs plugin on %s.', 'visual-regression-tests' ),
'<a href="' . esc_url( home_url() ) .'">' . esc_html( home_url() ) . '</a>'
);
?>
</p>
</td>
</tr>
</table>
</body>
</html>
@@ -0,0 +1,98 @@
@import "@wordpress/base-styles/colors.native"; // stylelint-disable-line scss/at-import-partial-extension
#vrts_post_options_metabox { // stylelint-disable-line selector-id-pattern
figcaption {
> :first-child {
margin-top: 0;
}
> :last-child {
margin-bottom: 0;
}
}
.figure {
margin: 0;
&-link {
outline: none;
box-shadow: none;
}
&-title {
margin-bottom: 0.5rem;
}
&-image {
aspect-ratio: 16/9;
border: solid 1px #bfbfbf;
width: 100%;
height: 100%;
object-fit: cover;
object-position: center top;
}
.howto {
margin-top: 0.5rem;
}
}
.vrts-testing-status {
display: flex;
justify-content: space-between;
gap: 1rem;
margin-bottom: 0;
&--paused {
color: $alert-red;
}
&--waiting {
color: #f97b06;
}
&--running {
color: $alert-green;
}
&-wrapper {
display: flex;
flex-direction: column;
gap: 5px;
margin: 1.5rem 0;
> * {
margin: 0;
}
.dashicons {
display: none;
}
}
&-info {
display: flex;
justify-content: space-between;
gap: 5px;
font-size: 0.75rem;
color: $gray-700;
}
}
.settings {
&-title {
margin-bottom: 0.5rem;
display: flex;
gap: 0.5rem;
justify-content: space-between;
align-items: center;
}
textarea {
width: 100%;
}
}
}
@@ -0,0 +1,101 @@
<?php
if ( ! $data['is_connected'] ) {
include_once __DIR__ . '/metabox-notifications/metabox-notification-connection-failed.php';
return;
}
?>
<?php wp_nonce_field( $data['nonce'], $data['nonce'] ); ?>
<p class="vrts-testing-toogle">
<?php if ( 0 === intval( $data['remaining_tests'] ) && ! $data['run_tests_checked'] ) { ?>
<input class="widefat" type="checkbox" id="<?php echo esc_attr( $data['field_test_status_key'] ); ?>" value="0" disabled />
<?php } else { ?>
<input class="widefat" type="checkbox" name="<?php echo esc_attr( $data['field_test_status_key'] ); ?>" id="<?php echo esc_attr( $data['field_test_status_key'] ); ?>" <?php checked( $data['run_tests_checked'] ); ?> value="1" />
<?php } ?>
<label for="<?php echo esc_attr( $data['field_test_status_key'] ); ?>"><strong><?php esc_html_e( 'Add to VRTs', 'visual-regression-tests' ); ?></strong></label>
<span class="vrts-tooltip">
<span class="vrts-tooltip-icon dashicons dashicons-info-outline"></span>
<span class="vrts-tooltip-content">
<span class="vrts-tooltip-content-inner">
<?php
printf(
/* translators: %1$s, %2$s: link wrapper. */
wp_kses_post( __( 'Add this page to your Visual Regression Tests for consistent checks to ensure no visual changes go unnoticed. Explore the %1$sTests page%2$s in the VRTs plugin for an overview of all tests and their status.', 'visual-regression-tests' ) ),
'<a href="' . esc_url( $data['plugin_url'] ) . '">',
'</a>'
);
?>
</span>
</span>
</span>
</p>
<?php
// Notification: New Test added.
if ( true === $data['is_new_test'] ) {
include_once __DIR__ . '/metabox-notifications/metabox-notification-new-test-added.php';
} elseif ( 1 === intval( $data['remaining_tests'] ) ) {
// Notification: Unlock more tests.
include_once __DIR__ . '/metabox-notifications/metabox-notification-unlock-more-tests.php';
} elseif ( 0 === intval( $data['remaining_tests'] ) ) {
// Notification: Unlock more tests.
include_once __DIR__ . '/metabox-notifications/metabox-notification-upgrade-required.php';
}
?>
<?php
// Display details only when "Run Tests" checkbox is active.
if ( $data['run_tests_checked'] ) {
$test_status = $data['test_status'];
$screenshot = $data['screenshot'];
?>
<div class="vrts-testing-status-wrapper">
<p class="vrts-testing-status">
<span><?php esc_html_e( 'Test Status', 'visual-regression-tests' ); ?></span>
<strong class="vrts-testing-status--<?php echo esc_attr( $test_status['class'] ); ?>"><?php echo wp_kses_post( $test_status['text'] ); ?></strong>
</p>
<p class="vrts-testing-status-info">
<?php echo wp_kses_post( $test_status['instructions'] ); ?>
</p>
</div>
<div class="vrts-testing-status-wrapper">
<p class="vrts-testing-status">
<span><?php esc_html_e( 'Snapshot', 'visual-regression-tests' ); ?></span>
<span class="vrts-testing-status-info">
<?php
$text = in_array( $screenshot['status'], [ 'paused', 'waiting' ], true ) ? $screenshot['text'] : $screenshot['instructions'];
echo wp_kses_post( $text );
?>
</span>
</p>
<figure class="figure">
<?php echo wp_kses_post( $screenshot['screenshot'] ); ?>
</figure>
</div>
<div class="settings">
<input name="test_id" type="hidden" value="<?php echo esc_html( $data['test_settings']['test_id'] ); ?>"/>
<label for="vrts-hide-css-selectors" class="settings-title">
<span><?php esc_html_e( 'Hide elements from VRTs', 'visual-regression-tests' ); ?></span>
<span class="vrts-tooltip">
<span class="vrts-tooltip-icon dashicons dashicons-info-outline"></span>
<span class="vrts-tooltip-content">
<span class="vrts-tooltip-content-inner">
<?php
printf(
/* translators: %1$s, %2$s: link wrapper. */
esc_html__( 'Exclude elements on this page: Add %1$sCSS selectors%2$s (as comma separated list) to exclude elements from VRTs when a new snapshot gets created.', 'visual-regression-tests' ),
'<a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors" target="_blank">',
'</a>'
);
?>
</span>
</span>
</span>
</label>
<textarea id="vrts-hide-css-selectors" name="hide_css_selectors" placeholder="<?php esc_html_e( 'e.g.: .lottie, #ads', 'visual-regression-tests' ); ?>" rows="4"><?php echo esc_html( $data['test_settings']['hide_css_selectors'] ); ?></textarea>
</div>
<?php }//end if
?>
@@ -0,0 +1,10 @@
<?php
use Vrts\Core\Utilities\Url_Helpers;
?>
<div class="vrts-metabox-notice vrts-metabox-notice-is-error">
<p><strong><?php esc_html_e( 'Connection failed', 'visual-regression-tests' ); ?></strong></p>
<p><?php esc_html_e( 'Something went wrong while trying to connect to the external service.', 'visual-regression-tests' ); ?></p>
<p><a href="<?php echo esc_attr( Url_Helpers::get_page_url( 'tests' ) ); ?>"><?php esc_html_e( 'Go to plugin page', 'visual-regression-tests' ); ?></a></p>
</div>
@@ -0,0 +1,12 @@
<div class="vrts-metabox-notice vrts-metabox-notice-is-success">
<p><strong><?php esc_html_e( 'You have added a new test', 'visual-regression-tests' ); ?></strong></p>
<p>
<?php
printf(
/* translators: %s: page title. */
esc_html__( 'The Visual Regression Test for the page %s has been added!', 'visual-regression-tests' ),
'<strong>' . esc_html( get_the_title( $data['post_id'] ) ) . '</strong>'
);
?>
</p>
</div>
@@ -0,0 +1,27 @@
<?php
use Vrts\Core\Utilities\Url_Helpers;
?>
<div class="vrts-metabox-notice vrts-metabox-notice-is-info">
<p><strong><?php esc_html_e( 'Unlock more tests', 'visual-regression-tests' ); ?></strong></p>
<p>
<?php
printf(
'%s %s',
sprintf(
/* translators: %1$s, %2$s: number of tests. */
esc_html__( 'Good work! You have added %1$s of %2$s available tests.', 'visual-regression-tests' ),
intval( $data['total_tests'] ) - intval( $data['remaining_tests'] ),
intval( $data['total_tests'] )
),
sprintf(
/* translators: %1$s, %2$s: link wrapper. */
esc_html__( 'Upgrade %1$shere%2$s to add more tests to your website!', 'visual-regression-tests' ),
'<a href="' . esc_url( Url_Helpers::get_page_url( 'upgrade' ) ) . '" target="_blank">',
'</a>'
)
)
?>
</p>
</div>
@@ -0,0 +1,18 @@
<?php
use Vrts\Core\Utilities\Url_Helpers;
?>
<div class="vrts-metabox-notice vrts-metabox-notice-is-error">
<p><strong><?php esc_html_e( 'Ready for an Upgrade?', 'visual-regression-tests' ); ?></strong></p>
<p>
<?php
printf(
'%1$s <a href="%2$s" target="_blank" title="%3$s">%3$s</a>',
esc_html__( 'Looks like you need a bigger plan to add more tests.', 'visual-regression-tests' ),
esc_url( Url_Helpers::get_page_url( 'upgrade' ) ),
esc_html__( 'Upgrade here!', 'visual-regression-tests' )
);
?>
</p>
</div>
@@ -0,0 +1,83 @@
.vrts-modal {
animation: vrts-fade-in 200ms both;
display: flex;
padding: 15px;
outline: 0;
z-index: 9999999999;
&[aria-hidden="true"] {
display: none;
}
&,
&__overlay {
bottom: 0;
left: 0;
position: fixed;
right: 0;
top: 0;
}
&__overlay {
background-color: rgba(44, 51, 56, 0.35);
}
&__content {
animation: vrts-fade-in 300ms both, vrts-slide-up 300ms both;
background-color: #fff;
box-shadow: 2px 2px 8px 4px rgba(30, 30, 30, 0.12);
border-radius: 3px;
display: flex;
flex-direction: column;
margin: auto;
max-height: min(calc(100vh - 40px), 620px);
min-width: 250px;
max-width: 345px;
position: relative;
width: 100%;
z-index: 99999;
&-inner {
flex: 1 1 auto;
overflow: auto;
padding: 10px 15px 15px;
}
}
&__close {
all: unset;
position: absolute;
top: 7px;
right: 7px;
width: 32px;
height: 32px;
cursor: pointer;
display: block !important;
background-image: url('data:image/svg+xml,<svg width="11" height="10" viewBox="0 0 11 10" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M10.2564 1.41L6.71644 4.95L10.2564 8.49L8.84645 9.9L5.30645 6.37L1.77644 9.9L0.356445 8.48L3.88645 4.95L0.356445 1.42L1.77644 0L5.30645 3.53L8.84645 0L10.2564 1.41Z" fill="%23757575"/></svg>'); // stylelint-disable-line
background-position: center;
background-repeat: no-repeat;
border-radius: 3px;
font-size: 0;
z-index: 1;
transition: background-color 0.2s;
&:hover {
background-color: #f6f7f7;
}
&:focus-visible {
outline: 2px solid var(--vrts-admin-theme-color);
}
}
&__title {
display: flex;
align-items: center;
gap: 0.5rem;
margin: 0;
padding: 15px 45px 0 15px;
font-size: 1rem;
font-weight: 600;
line-height: 1.5;
}
}
@@ -0,0 +1,13 @@
import A11yDialog from 'a11y-dialog';
class VrtsModal extends window.HTMLElement {
connectedCallback() {
this.modal = new A11yDialog( this );
}
disconnectedCallback() {
this.modal.destroy();
}
}
window.customElements.define( 'vrts-modal', VrtsModal );
@@ -0,0 +1,66 @@
.vrts-settings {
display: block;
h1 {
margin-bottom: 20px;
}
form h2 {
font-weight: 400;
font-size: 20px;
span {
display: block;
margin-top: 0.75rem;
font-size: 13px;
line-height: 1.5;
font-weight: 400;
}
}
fieldset {
display: flex !important;
align-items: center;
gap: 0.5rem;
}
input {
&[readonly] {
border-color: #8c8f94;
background-color: #fff;
color: #2c3338;
}
}
.form-table {
th,
td {
padding: 0 24px 24px 0;
}
td fieldset label {
margin: 0 !important;
}
}
&__pro-label {
display: inline-block;
padding: 0.25rem 0.5rem;
font-size: 11px;
line-height: 1;
font-weight: 600;
text-transform: uppercase;
color: #fff;
background: #757575;
border-radius: 35px;
}
&__modal {
&-info {
margin-top: 0;
}
}
}
@@ -0,0 +1,28 @@
<?php
use Vrts\Core\Utilities\Url_Helpers;
?>
<vrts-settings class="wrap vrts-settings">
<h1><?php echo esc_html( $data['title'] ); ?></h1>
<form method="post" action="options.php">
<?php
settings_fields( $data['settings_fields'] );
do_settings_sections( $data['settings_sections'] );
submit_button();
?>
</form>
<vrts-modal id="vrts-modal-pro-settings" class="vrts-modal vrts-settings__modal" aria-hidden="true">
<div class="vrts-modal__overlay" data-a11y-dialog-hide></div>
<div class="vrts-modal__content" role="document">
<button type="button" class="vrts-modal__close" data-a11y-dialog-hide aria-label="<?php esc_attr_e( 'Close', 'visual-regression-tests' ); ?>"></button>
<h2 class="vrts-modal__title">
<?php esc_html_e( '🚀 Go Pro for more testing power!', 'visual-regression-tests' ); ?>
</h2>
<div class="vrts-modal__content-inner">
<p class="vrts-settings__modal-info"><?php esc_html_e( 'Upgrade your plan and unlock automatic testing after updates, plus more tools like manual testing to keep your sites running smoothly.', 'visual-regression-tests' ); ?></p>
<a href="<?php echo esc_url( Url_Helpers::get_page_url( 'upgrade' ) ); ?>" class="button button-primary vrts-settings__modal-link"><?php esc_html_e( 'Unlock Now', 'visual-regression-tests' ); ?></a>
</div>
</div>
</vrts-modal>
</vrts-settings>
@@ -0,0 +1,36 @@
class VrtsSettings extends window.HTMLElement {
constructor() {
super();
this.resolveElements();
this.bindFunctions();
this.bindEvents();
}
resolveElements() {
this.$proSettingsCheckboxes = this.querySelectorAll(
'[data-a11y-dialog-show] input[type="checkbox"]'
);
}
bindFunctions() {
this.onCheckboxChange = this.onCheckboxChange.bind( this );
}
bindEvents() {
this.$proSettingsCheckboxes?.forEach( ( item ) => {
item.addEventListener( 'change', this.onCheckboxChange );
} );
}
onCheckboxChange( e ) {
e.preventDefault();
e.currentTarget.checked = ! e.currentTarget.checked;
}
disconnectedCallback() {
this.$proSettingsCheckboxes?.forEach( ( item ) => {
item.removeEventListener( 'change', this.onCheckboxChange );
} );
}
}
window.customElements.define( 'vrts-settings', VrtsSettings );
@@ -0,0 +1,206 @@
@import "@wordpress/base-styles/colors.native"; // stylelint-disable-line scss/at-import-partial-extension
.vrts-test-run-alerts {
&__heading {
display: flex;
align-items: center;
justify-content: space-between;
background: #f0f0f1;
padding: 0;
height: 62px;
overflow: hidden;
transition: box-shadow 0.2s;
&[data-is-sticky="true"] {
box-shadow: 0 1px 0 0 $gray-10;
}
a {
text-decoration: none;
}
> * {
display: flex;
align-items: center;
gap: 0.25rem;
}
@media (min-width: 851px) {
padding: 0 20px;
position: sticky;
top: -0.1px;
z-index: 3;
}
&-link {
cursor: pointer;
position: relative;
background: transparent;
border: 0;
padding: 0;
min-height: 30px;
transition: color 0.2s;
&::before {
content: "";
position: absolute;
top: -5px;
left: -15px;
right: -15px;
bottom: -5px;
}
&:hover {
color: $blue-50;
}
&--button {
color: inherit;
}
}
}
&__list {
display: flex;
gap: 10px;
padding: 0 10px 10px 10px;
margin: 0 -10px;
overflow-x: scroll;
-webkit-overflow-scrolling: touch;
@media (min-width: 851px) {
flex-direction: column;
overflow: visible;
padding: 0 20px;
margin: 0 0 20px 0;
}
}
&__card {
box-sizing: border-box;
background: #fff;
border: 1px solid $gray-10;
display: flex;
flex-direction: column;
flex: 1 0 auto;
transition: all 0.1s;
box-shadow: inset 0 0 0 1px transparent;
width: 60%;
@media (min-width: 480px) {
width: 40%;
}
@media (min-width: 600px) {
width: 30%;
}
@media (min-width: 851px) {
width: 100%;
}
&:has([data-vrts-state="read"]) {
background: #f6f7f9;
}
&:has(:hover),
&:has([data-vrts-current="true"]) {
border-color: var(--vrts-admin-theme-color);
box-shadow: inset 0 0 0 1px var(--vrts-admin-theme-color);
}
&:has([data-vrts-current="false"]:focus-visible) {
border-color: $gray-20;
box-shadow: inset 0 0 0 1px $gray-20;
}
&-link {
display: block;
text-decoration: none;
padding: 10px 10px 0 10px;
&:focus {
box-shadow: none;
outline: none;
}
}
&-figure {
position: relative;
background: #f6f7f7;
border: 1px solid $gray-5;
aspect-ratio: 1 / 1.15;
display: flex;
align-items: center;
justify-content: center;
margin: 0;
width: 100%;
}
&-flag {
position: absolute;
top: 10px;
left: 10px;
visibility: hidden;
[data-vrts-false-positive="true"] & {
visibility: visible;
}
}
&-image {
position: absolute;
max-width: 100%;
max-height: 100%;
}
&-title-inner,
&-path {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&-title {
display: flex;
align-items: center;
gap: 0.3125rem;
color: $gray-70;
font-weight: 700;
margin-top: 12px;
margin-bottom: 4px;
text-decoration: none;
[data-vrts-state="read"] & {
font-weight: 400;
}
[data-vrts-state="unread"] &::after {
content: "";
width: 8px;
height: 8px;
background: var(--vrts-admin-theme-color);
border-radius: 50%;
flex: 0 0 auto;
margin-top: 1px;
}
}
&-path {
padding: 10px;
margin-top: -10px;
&:focus {
box-shadow: none;
outline: none;
}
&:focus-visible {
outline: 2px solid $gray-20;
outline-offset: -8px;
text-decoration: none;
}
}
}
}
@@ -0,0 +1,116 @@
<?php
use Vrts\Models\Alert;
use Vrts\Core\Utilities\Image_Helpers;
use Vrts\Core\Utilities\Url_Helpers;
$unread_alerts = Alert::get_unread_count_by_test_run_ids( $data['run']->id );
$unread_count = $unread_alerts[0]->count ?? 0;
$unred_runs_count = Alert::get_total_items_grouped_by_test_run();
$current_alert_id = isset( $data['alert']->id ) ? $data['alert']->id : 0;
?>
<vrts-test-run-alerts
class="vrts-test-run-alerts"
data-vrts-current-alert="<?php echo esc_attr( $current_alert_id ? $current_alert_id : 'false' ); ?>"
data-vrts-unread-runs="<?php echo esc_attr( $unred_runs_count ); ?>">
<div class="vrts-test-run-alerts__heading">
<a href="<?php echo esc_url( Url_Helpers::get_page_url( 'runs' ) ); ?>" class="vrts-test-run-alerts__heading-link">
<?php vrts()->icon( 'chevron-left' ); ?>
<?php esc_html_e( 'All Runs', 'visual-regression-tests' ); ?>
</a>
<?php if ( $data['alerts'] ) : ?>
<button data-vrts-loading="false" data-vrts-action-state="<?php echo esc_attr( $unread_count > 0 ? 'primary' : 'secondary' ); ?>" data-vrts-test-run-id="<?php echo esc_attr( $data['run']->id ); ?>" data-vrts-test-run-action="read-status" class="vrts-test-run-alerts__heading-link vrts-test-run-alerts__heading-link--button vrts-action-button">
<span class="vrts-action-button__icons">
<span class="vrts-action-button__icon" data-vrts-action-state-secondary><?php vrts()->icon( 'email-unread' ); ?></span>
<span class="vrts-action-button__icon" data-vrts-action-state-primary><?php vrts()->icon( 'email-read' ); ?></span>
<span class="vrts-action-button__spinner"><?php vrts()->icon( 'spinner' ); ?></span>
</span>
<span class="vrts-action-button__info" data-vrts-action-state-primary><?php esc_html_e( 'Mark all as read', 'visual-regression-tests' ); ?></span>
<span class="vrts-action-button__info" data-vrts-action-state-secondary><?php esc_html_e( 'Mark all as unread', 'visual-regression-tests' ); ?></span>
</button>
<?php endif; ?>
</div>
<?php if ( $data['alerts'] ) : ?>
<div class="vrts-test-run-alerts__list">
<?php
foreach ( $data['alerts'] as $alert ) :
$test = null;
foreach ( $data['tests'] as $some_test ) {
if ( $some_test['post_id'] === $alert->post_id ) {
$test = $some_test;
break;
}
}
$alert_link = add_query_arg( [
'run_id' => $data['run']->id,
'alert_id' => $alert->id,
], Url_Helpers::get_page_url( 'runs' ) );
$alert_permalink = '';
$alert_post_title = '';
if ( $test ) {
$alert_permalink = $test['permalink'];
$alert_post_title = $test['post_title'];
}
if ( ! $alert_permalink ) {
$alert_permalink = get_permalink( $alert->post_id );
}
if ( ! $alert_post_title ) {
$alert_post_title = get_the_title( $alert->post_id ) ?: 'N/A';
}
$alert_relative_permalink = Url_Helpers::make_relative( $alert_permalink );
?>
<div class="vrts-test-run-alerts__card">
<a
id="vrts-alert-<?php echo esc_attr( $alert->id ); ?>"
href="<?php echo esc_url( $alert_link ); ?>"
class="vrts-test-run-alerts__card-link"
data-vrts-alert="<?php echo esc_attr( $alert->id ); ?>"
data-vrts-current="<?php echo esc_attr( $current_alert_id === $alert->id ? 'true' : 'false' ); ?>"
data-vrts-state="<?php echo esc_attr( intval( $alert->alert_state ) === 0 ? 'unread' : 'read' ); ?>"
data-vrts-false-positive="<?php echo esc_attr( $alert->is_false_positive ? 'true' : 'false' ); ?>">
<figure class="vrts-test-run-alerts__card-figure">
<img class="vrts-test-run-alerts__card-image" src="<?php echo esc_url( Image_Helpers::get_screenshot_url( $alert, 'comparison', 'preview' ) ); ?>" alt="<?php esc_attr_e( 'Difference', 'visual-regression-tests' ); ?>">
<span class="vrts-test-run-alerts__card-flag"><?php vrts()->icon( 'flag' ); ?></span>
</figure>
<span class="vrts-test-run-alerts__card-title">
<span class="vrts-test-run-alerts__card-title-inner"><?php echo esc_html( $alert_post_title ); ?></span>
</span>
</a>
<a href="<?php echo esc_url( get_permalink( $alert->post_id ) ); ?>" target="_blank" class="vrts-test-run-alerts__card-path"><?php echo esc_html( $alert_relative_permalink ); ?></a>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php vrts()->component( 'test-run-receipt', $data ); ?>
<script>
const urlParams = new URLSearchParams( window.location.search );
const currentAlertId = urlParams.get( 'alert_id' );
if ( currentAlertId ) {
const $sidebar = document.querySelector(
'.vrts-test-run-page__sidebar'
);
let $alert = document.getElementById(
`vrts-alert-${ currentAlertId }`
);
let offsetTop = 0;
while ( $alert && $alert !== $sidebar ) {
offsetTop += $alert.offsetTop;
$alert = $alert.offsetParent;
}
if ( $alert ) {
$sidebar.scrollTo( {
left: 0,
top: offsetTop - 82,
} );
}
}
</script>
</vrts-test-run-alerts>
@@ -0,0 +1,297 @@
class VrtsTestRunAlerts extends window.HTMLElement {
constructor() {
super();
this.resolveElements();
this.bindFunctions();
this.bindEvents();
this.unreadAlerts = new Set();
this.initialUnreadAlerts = 0;
this.unreadRuns = parseInt(
this.getAttribute( 'data-vrts-unread-runs' ),
10
);
}
resolveElements() {
this.$heading = this.querySelector( '.vrts-test-run-alerts__heading' );
this.$alerts = this.querySelectorAll( '[data-vrts-alert]' );
this.$actionButtons = this.querySelectorAll(
'[data-vrts-test-run-action]'
);
this.$runsMenuItems = [
document.querySelector(
'.vrts-admin-header [href*="page=vrts-runs"]'
),
document.querySelector(
'#adminmenu .menu-top[href*="page=vrts"] .wp-menu-name'
),
document.querySelector(
'#adminmenu .wp-submenu a[href*="page=vrts-runs"]'
),
];
}
bindFunctions() {
this.handleAlertClick = this.handleAlertClick.bind( this );
this.handleActionClick = this.handleActionClick.bind( this );
this.updateRunsCount = this.updateRunsCount.bind( this );
}
bindEvents() {
this.$alerts?.forEach( ( item ) => {
item.addEventListener( 'click', this.handleAlertClick );
} );
this.$actionButtons?.forEach( ( item ) => {
item.addEventListener( 'click', this.handleActionClick );
} );
}
connectedCallback() {
this.checkHeadingSticky();
this.checkReadStatusChange();
this.$alerts.forEach( ( item ) => {
const isUnread =
item.getAttribute( 'data-vrts-state' ) === 'unread';
if ( isUnread ) {
this.unreadAlerts.add( item.getAttribute( 'data-vrts-alert' ) );
}
} );
this.initialUnreadAlerts = this.unreadAlerts.size;
}
checkHeadingSticky() {
const checkIsSticky = ( entries ) => {
const isSticky = ! entries[ 0 ].isIntersecting;
this.$heading.setAttribute( 'data-is-sticky', isSticky );
};
const observer = new window.IntersectionObserver( checkIsSticky, {
root: document,
threshold: [ 1 ],
} );
observer.observe( this.$heading );
}
checkReadStatusChange() {
const observer = new window.MutationObserver( ( mutations ) => {
mutations.forEach( ( mutation ) => {
if (
mutation.type === 'attributes' &&
mutation.attributeName === 'data-vrts-state'
) {
const id =
mutation.target.getAttribute( 'data-vrts-alert' );
const state =
mutation.target.getAttribute( 'data-vrts-state' );
if ( id === 'receipt' ) {
return;
}
if ( 'unread' === state ) {
this.unreadAlerts.add( id );
} else {
this.unreadAlerts.delete( id );
}
this.querySelector(
'[data-vrts-test-run-action="read-status"]'
).setAttribute(
'data-vrts-action-state',
this.unreadAlerts.size > 0 ? 'primary' : 'secondary'
);
this.updateRunsCount( this.unreadAlerts.size );
}
} );
} );
this.$alerts.forEach( ( item ) => {
observer.observe( item, {
attributes: true,
} );
} );
}
updateRunsCount( unreadAlerts ) {
let unreadRuns = this.unreadRuns;
if ( unreadAlerts > 0 && this.initialUnreadAlerts === 0 ) {
unreadRuns += 1;
} else if ( unreadAlerts === 0 && this.initialUnreadAlerts > 0 ) {
unreadRuns -= 1;
}
this.$runsMenuItems.forEach( ( item ) => {
if ( item ) {
item.querySelector( '.update-plugins' )?.remove();
item.textContent = item.textContent.replace( /\u00A0/g, '' );
if ( unreadRuns > 0 ) {
const $count = document.createElement( 'span' );
const nbsp = document.createTextNode( '\u00A0' );
$count.classList.add( 'update-plugins' );
$count.textContent = unreadRuns;
item.appendChild( nbsp );
item.appendChild( $count );
}
}
} );
}
handleAlertClick( e ) {
e.preventDefault();
const $el = e.currentTarget;
const isCurrent = $el.getAttribute( 'data-vrts-current' ) === 'true';
if ( isCurrent ) {
return;
}
const href = $el.getAttribute( 'href' );
const $content =
document.querySelector( 'vrts-comparisons' ) ||
document.querySelector( 'vrts-test-run-success' );
const $pagination = document.querySelector(
'vrts-test-run-pagination'
);
this.$alerts.forEach( ( item ) => {
item.setAttribute( 'data-vrts-current', 'false' );
} );
$el.setAttribute( 'data-vrts-current', 'true' );
const timeout = setTimeout( () => {
$content.setAttribute( 'data-vrts-loading', 'true' );
}, 200 );
fetch( href )
.then( ( response ) => {
return response.text();
} )
.then( ( data ) => {
const parser = new window.DOMParser();
const $html = parser.parseFromString( data, 'text/html' );
const $newContent =
$html.querySelector( 'vrts-comparisons' ) ||
$html.querySelector( 'vrts-test-run-success' );
const $newPagination = $html.querySelector(
'vrts-test-run-pagination'
);
window.history.replaceState( {}, '', href );
this.scrollTo( $content.offsetTop - 62 );
if ( $newContent ) {
$content.replaceWith( $newContent );
}
if ( $newPagination ) {
$pagination.replaceWith( $newPagination );
}
clearTimeout( timeout );
} );
}
handleActionClick( e ) {
const $el = e.currentTarget;
const isLoading = $el.getAttribute( 'data-vrts-loading' ) === 'true';
const state = $el.getAttribute( 'data-vrts-action-state' );
const isPrimary = state === 'primary';
if ( isLoading ) {
return;
}
const action = $el.getAttribute( 'data-vrts-test-run-action' );
const id = $el.getAttribute( 'data-vrts-test-run-id' );
this.handleAction( action, $el, id, isPrimary );
}
handleAction( action, $el, id, shouldSetAction ) {
const restEndpoint = `${ window.vrts_admin_vars.rest_url }/test-runs/${ id }/${ action }`;
const method = shouldSetAction ? 'POST' : 'DELETE';
let loadingElapsedTime = 0;
let interval = null;
const timeout = setTimeout( () => {
$el.setAttribute( 'data-vrts-loading', 'true' );
const loadingStartTime = window.Date.now();
interval = setInterval( () => {
loadingElapsedTime = window.Date.now() - loadingStartTime;
}, 50 );
}, 200 );
fetch( restEndpoint, {
method,
headers: {
'X-WP-Nonce': window.vrts_admin_vars.rest_nonce,
},
} )
.then( ( response ) => {
return response.json();
} )
.then( () => {
const loadingTimeoutTime =
loadingElapsedTime > 0
? Math.abs( loadingElapsedTime - 400 )
: 0;
setTimeout( () => {
$el.setAttribute( 'data-vrts-loading', 'false' );
$el.setAttribute(
'data-vrts-action-state',
shouldSetAction ? 'secondary' : 'primary'
);
const $alerts =
document.querySelectorAll( '[data-vrts-alert]' );
$alerts.forEach( ( item ) => {
item.setAttribute(
'data-vrts-state',
shouldSetAction ? 'read' : 'unread'
);
} );
}, loadingTimeoutTime );
clearTimeout( timeout );
clearInterval( interval );
} );
}
scrollTo( offset ) {
const $el =
document.fullscreenElement ||
document.webkitFullscreenElement ||
document.msFullscreenElement ||
window;
$el.scrollTo( {
top: offset,
behavior: 'smooth',
} );
}
disconnectedCallback() {
this.$alerts?.forEach( ( item ) => {
item.removeEventListener( 'click', this.handleAlertClick );
} );
this.$actionButtons?.forEach( ( item ) => {
item.removeEventListener( 'click', this.handleActionClick );
} );
}
}
window.customElements.define( 'vrts-test-run-alerts', VrtsTestRunAlerts );
@@ -0,0 +1,44 @@
@import "@wordpress/base-styles/colors.native"; // stylelint-disable-line scss/at-import-partial-extension
.vrts-test-run-info {
display: flex;
align-items: center;
gap: 0.5rem;
@media (max-width: 700px) {
flex-wrap: wrap;
}
> * {
display: flex;
align-items: center;
gap: 0.5rem;
min-width: 0;
flex-shrink: 0;
&:not(:last-child)::after {
content: "";
display: block;
width: 1px;
height: 1rem;
background: $gray-10;
}
}
&__trigger {
flex-shrink: 1;
}
.vrts-test-run-trigger-notes {
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
display: -webkit-box;
color: $gray-40;
font-size: 13px;
line-height: 1.4;
max-block-size: calc(13px * 1.4);
overflow: hidden;
text-overflow: ellipsis;
white-space: initial;
}
}
@@ -0,0 +1,27 @@
<?php
use Vrts\Core\Utilities\Date_Time_Helpers;
use Vrts\Models\Test_Run;
$trigger_note = Test_Run::get_trigger_note( $data );
?>
<div class="vrts-test-run-info">
<strong>
<?php
// translators: %s: the run number.
printf( esc_html__( 'Run #%s', 'visual-regression-tests' ), esc_html( $data->id ) );
?>
</strong>
<?php echo wp_kses( Date_Time_Helpers::get_formatted_relative_date_time( $data->finished_at ), [ 'time' => [ 'datetime' => true ] ] ); ?>
<span class="vrts-test-run-info__trigger">
<span class="vrts-test-run-trigger vrts-test-run-trigger--<?php echo esc_attr( $data->trigger ); ?>">
<?php echo esc_html( Test_Run::get_trigger_title( $data ) ); ?>
</span>
<?php if ( ! empty( $trigger_note ) ) : ?>
<span class="vrts-test-run-trigger-notes" title="<?php echo esc_attr( $trigger_note ); ?>">
<?php echo esc_html( $trigger_note ); ?>
</span>
<?php endif; ?>
</span>
</div>
@@ -0,0 +1,128 @@
@import "@wordpress/base-styles/colors.native"; // stylelint-disable-line scss/at-import-partial-extension
@import "@wordpress/base-styles/mixins";
@import "@wordpress/base-styles/breakpoints";
@import "@wordpress/base-styles/variables";
@import "@wordpress/base-styles/default-custom-properties";
.vrts-test-run-wrap {
#wpfooter {
display: none;
}
#wpbody-content {
padding-bottom: 0;
@media (min-width: 851px) {
position: sticky;
top: 0;
}
}
}
.vrts-test-run-page {
--vrts-test-run-page-offset: 54px; // our header height
display: flex;
margin-right: 10px;
flex-direction: column;
min-height: 100vh;
@media (min-width: 851px) {
gap: 20px;
flex-direction: row;
margin-left: -20px;
margin-right: 20px;
}
&__sidebar {
order: 2;
&::-webkit-scrollbar {
width: 2px;
height: 2px;
}
&::-webkit-scrollbar-track {
background: linear-gradient(to right, #{$gray-10} 1px, transparent 1px);
}
&::-webkit-scrollbar-thumb {
background: $gray-20;
}
@media (min-width: 851px) {
order: -1;
width: 280px;
position: sticky;
top: var(--wp-admin--admin-bar--height);
height: calc(100vh - var(--wp-admin--admin-bar--height) - var(--vrts-test-run-page-offset));
overflow-x: hidden;
overflow-y: scroll;
-webkit-overflow-scrolling: touch;
}
}
&__content {
flex: 1;
padding-top: 10px;
padding-bottom: 20px;
&[data-vrts-fullscreen="true"] {
overflow: auto;
padding: 0 20px 20px 20px;
background-color: #f0f0f1;
}
@media (min-width: 851px) {
padding-top: 0;
}
&-heading {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
column-gap: 2rem;
row-gap: 1rem;
padding: 1rem 0;
border-bottom: 1px solid $gray-10;
@media (min-width: 851px) {
box-sizing: border-box;
flex-wrap: nowrap;
position: sticky;
top: var(--wp-admin--admin-bar--height);
height: 62px;
padding: 0;
background: #f0f0f1;
z-index: 1;
[data-vrts-fullscreen="true"] & {
top: 0;
}
}
}
&-navigation-info {
display: none;
align-items: center;
justify-content: center;
gap: 0.25rem;
margin-top: 1rem;
color: #757575;
@media (min-width: 851px) {
display: flex;
}
svg {
color: $gray-70;
background: $gray-5;
border-radius: 3px;
padding: 5px;
width: 0.75rem;
height: 0.75rem;
}
}
}
}
@@ -0,0 +1,37 @@
<vrts-test-run-page class="vrts-test-run-page">
<div class="vrts-test-run-page__sidebar">
<?php vrts()->component( 'test-run-alerts', $data ); ?>
</div>
<div class="vrts-test-run-page__content" data-vrts-fullscreen="false">
<div class="vrts-test-run-page__content-heading">
<?php vrts()->component( 'test-run-info', $data['run'] ); ?>
<?php
vrts()->component( 'test-run-pagination', [
'run' => $data['run'],
'pagination' => $data['pagination'],
'is_receipt' => $data['is_receipt'],
] );
?>
</div>
<?php if ( $data['alerts'] && ! $data['is_receipt'] ) : ?>
<?php
vrts()->component( 'comparisons', [
'alert' => $data['alert'],
'test_settings' => $data['test_settings'],
] );
?>
<div class="vrts-test-run-page__content-navigation-info">
<?php esc_html_e( 'Navigate with arrow keys', 'visual-regression-tests' ); ?>
<?php vrts()->icon( 'arrow-up' ); ?>
<?php vrts()->icon( 'arrow-down' ); ?>
</div>
<?php else : ?>
<?php
vrts()->component( 'test-run-success', [
'run' => $data['run'],
'is_receipt' => $data['is_receipt'],
] );
?>
<?php endif; ?>
</div>
</vrts-test-run-page>
@@ -0,0 +1,50 @@
class VrtsTestRunPage extends window.HTMLElement {
constructor() {
super();
this.bindFunctions();
this.bindEvents();
this.isScrolling = false;
}
bindFunctions() {
this.onScroll = this.onScroll.bind( this );
this.setOffset = this.setOffset.bind( this );
}
bindEvents() {
document.addEventListener( 'scroll', this.onScroll );
}
onScroll() {
if ( ! this.isScrolling ) {
this.isScrolling = true;
window.requestAnimationFrame( this.setOffset );
}
}
setOffset() {
const offset = Math.max( 0, this.offset - window.scrollY );
this.style.setProperty(
'--vrts-test-run-page-offset',
`${ offset }px`
);
this.isScrolling = false;
}
connectedCallback() {
const rect = this.getBoundingClientRect();
const adminBarHeight =
document.getElementById( 'wpadminbar' ).offsetHeight;
this.offset = rect.top + window.scrollY - adminBarHeight;
this.setOffset();
}
disconnectedCallback() {
document.removeEventListener( 'scroll', this.onScroll );
}
}
window.customElements.define( 'vrts-test-run-page', VrtsTestRunPage );
@@ -0,0 +1,22 @@
.vrts-test-run-pagination {
display: flex;
gap: 0.25rem;
align-items: center;
&__text {
white-space: nowrap;
margin-right: 0.25rem;
}
.button {
display: inline-block;
vertical-align: baseline;
min-width: 30px;
min-height: 30px;
margin: 0;
padding: 0 4px;
font-size: 16px;
line-height: 1.625;
text-align: center;
}
}
@@ -0,0 +1,38 @@
<?php
use Vrts\Core\Utilities\Url_Helpers;
// don't show pagination if there is no previous or next alert.
if ( 0 === $data['pagination']['total'] ) {
return;
}
?>
<vrts-test-run-pagination class="vrts-test-run-pagination">
<span class="screen-reader-text"><?php esc_html_e( 'Current Page', 'visual-regression-tests' ); ?></span>
<span class="vrts-test-run-pagination__text">
<?php
if ( $data['is_receipt'] ) {
esc_html_e( 'Test Receipt', 'visual-regression-tests' );
} else {
printf(
/* translators: %d: pages. */
esc_html_x( 'Alert %1$d of %2$d', 'e.g. Alert 1 of 2', 'visual-regression-tests' ),
esc_html( $data['pagination']['current'] ),
esc_html( $data['pagination']['total'] )
);
}
?>
</span>
<a data-vrts-pagination="prev" data-vrts-alert-id="<?php echo esc_attr( $data['pagination']['prev_alert_id'] ); ?>" class="button <?php echo ( 0 === $data['pagination']['prev_alert_id'] ) ? 'button-disabled' : ''; ?>"
<?php echo ( 0 !== $data['pagination']['prev_alert_id'] ) ? 'href="' . esc_url( Url_Helpers::get_alert_page( $data['pagination']['prev_alert_id'], $data['run']->id ) ) . '"' : ''; ?>>
<span class="screen-reader-text"><?php esc_html_e( 'Previous alert', 'visual-regression-tests' ); ?></span>
<span aria-hidden="true"></span>
</a>
<a data-vrts-pagination="next" data-vrts-alert-id="<?php echo esc_attr( $data['pagination']['next_alert_id'] ); ?>" class="button <?php echo ( 0 === $data['pagination']['next_alert_id'] ) ? 'button-disabled' : ''; ?>"
<?php echo ( 0 !== $data['pagination']['next_alert_id'] ) ? 'href="' . esc_url( Url_Helpers::get_alert_page( $data['pagination']['next_alert_id'], $data['run']->id ) ) . '"' : ''; ?>>
<span class="screen-reader-text"><?php esc_html_e( 'Next alert', 'visual-regression-tests' ); ?></span>
<span aria-hidden="true"></span>
</a>
</vrts-test-run-pagination>
@@ -0,0 +1,152 @@
class VrtsTestRunPagination extends window.HTMLElement {
constructor() {
super();
this.resolveElements();
this.bindFunctions();
this.bindEvents();
}
resolveElements() {
this.$alerts = document.querySelectorAll( '[data-vrts-alert]' );
this.$buttons = this.querySelectorAll( '.button' );
}
bindFunctions() {
this.handleClick = this.handleClick.bind( this );
this.handleKeyDown = this.handleKeyDown.bind( this );
}
bindEvents() {
this.$buttons?.forEach( ( item ) => {
item.addEventListener( 'click', this.handleClick );
} );
document.addEventListener( 'keydown', this.handleKeyDown );
}
handleClick( e ) {
e.preventDefault();
const $el = e.currentTarget;
const nextAlertId = $el.getAttribute( 'data-vrts-alert-id' );
let $nextAlert = document.getElementById(
`vrts-alert-${ nextAlertId }`
);
if ( ! $nextAlert ) {
return;
}
const href = $el.getAttribute( 'href' );
const $content =
document.querySelector( 'vrts-comparisons' ) ||
document.querySelector( 'vrts-test-run-success' );
const $sidebar = document.querySelector(
'.vrts-test-run-page__sidebar'
);
this.$alerts.forEach( ( item ) => {
item.setAttribute( 'data-vrts-current', 'false' );
} );
$nextAlert.setAttribute( 'data-vrts-current', 'true' );
let loadingElapsedTime = 0;
let interval = null;
const timeout = setTimeout( () => {
$content.setAttribute( 'data-vrts-loading', 'true' );
const loadingStartTime = window.Date.now();
interval = setInterval( () => {
loadingElapsedTime = window.Date.now() - loadingStartTime;
}, 50 );
}, 200 );
let offsetTop = 0;
while ( $nextAlert && $nextAlert !== $sidebar ) {
offsetTop += $nextAlert.offsetTop;
$nextAlert = $nextAlert.offsetParent;
}
$sidebar.scrollTo( {
top: offsetTop - 82,
behavior: 'smooth',
} );
fetch( href )
.then( ( response ) => {
return response.text();
} )
.then( ( data ) => {
const parser = new window.DOMParser();
const $html = parser.parseFromString( data, 'text/html' );
const $newContent =
$html.querySelector( 'vrts-comparisons' ) ||
$html.querySelector( 'vrts-test-run-success' );
const $newPagination = $html.querySelector(
'vrts-test-run-pagination'
);
window.history.replaceState( {}, '', href );
this.scrollTo( $content.offsetTop - 62 );
const loadingTimeoutTime =
loadingElapsedTime > 0
? Math.abs( loadingElapsedTime - 400 )
: 0;
setTimeout( () => {
if ( $newContent ) {
$content.replaceWith( $newContent );
}
if ( $newPagination ) {
this.replaceWith( $newPagination );
}
}, loadingTimeoutTime );
clearTimeout( timeout );
clearInterval( interval );
} );
}
handleKeyDown( e ) {
if ( e.key === 'ArrowUp' ) {
e.preventDefault();
this.querySelector( '[data-vrts-pagination="prev"]' ).click();
}
if ( e.key === 'ArrowDown' ) {
e.preventDefault();
this.querySelector( '[data-vrts-pagination="next"]' ).click();
}
}
scrollTo( offset ) {
const $el =
document.fullscreenElement ||
document.webkitFullscreenElement ||
document.msFullscreenElement ||
window;
$el.scrollTo( {
top: offset,
behavior: 'smooth',
} );
}
disconnectedCallback() {
this.$buttons?.forEach( ( item ) => {
item.removeEventListener( 'click', this.handleClick );
} );
document.removeEventListener( 'keydown', this.handleKeyDown );
}
}
window.customElements.define(
'vrts-test-run-pagination',
VrtsTestRunPagination
);
@@ -0,0 +1,205 @@
@import "@wordpress/base-styles/colors.native"; // stylelint-disable-line scss/at-import-partial-extension
.vrts-test-run-receipt {
position: relative;
padding: 0 12px;
margin: 30px auto 30px auto;
background-color: #f6f7fb;
box-shadow: 0 0 6px 2px rgba(0, 0, 0, 0.07);
max-width: 280px;
transition: box-shadow 0.2s;
@media (min-width: 851px) {
max-width: 100%;
margin: 8px 20px 20px;
}
&:has(.vrts-test-run-receipt__link:hover),
&:has(.vrts-test-run-receipt__link:focus-visible) {
box-shadow: 0 0 6px 2px rgba(0, 0, 0, 0.15);
}
&::before,
&::after {
--vrts-test-run-receipt-mask: radial-gradient(10px at top, #0000 97%, #000) 50% / 18.5px 100%;
position: absolute;
left: 0;
right: 0;
bottom: 100%;
content: "";
mask: var(--vrts-test-run-receipt-mask);
display: block;
height: 10px;
background: inherit;
}
&::after {
--vrts-test-run-receipt-mask: radial-gradient(10px at bottom, #0000 97%, #000) 50% / 18.5px 100%;
bottom: -10px;
}
> * {
padding: 12px 0;
&:where(:not(:last-child)) {
border-bottom: 1px dashed #c3c4c7;
}
}
> svg {
width: 100%;
padding: 0;
position: absolute;
bottom: 100%;
left: 0;
}
a {
z-index: 2;
}
&__link {
display: block;
border: none;
position: absolute;
inset: 0;
&:focus {
outline: 0;
box-shadow: none;
}
}
&__header {
display: flex;
flex-direction: column;
gap: 10px;
padding-top: 20px;
&-logo {
display: flex;
align-items: center;
gap: 0.5rem;
justify-content: center;
font-weight: 600;
font-size: 15px;
svg {
width: 20px;
height: 20px;
path {
fill: #2c3338;
}
}
}
&-info {
font-size: 14px;
display: flex;
flex-direction: column;
text-align: center;
gap: 2px;
a {
font-size: 11px;
}
}
}
&__info {
display: flex;
flex-wrap: wrap;
column-gap: 10px;
justify-content: center;
font-size: 11px;
}
&__pages-status,
&__total {
line-height: 16px;
display: flex;
flex-direction: column;
gap: 4px;
&-heading,
&-row {
display: flex;
justify-content: space-between;
gap: 10px;
}
&-heading {
font-size: 12px;
font-weight: 700;
line-height: 1.6;
}
&-row {
font-size: 11px;
}
a {
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
display: -webkit-box;
color: inherit;
max-block-size: 16px;
overflow: hidden;
text-overflow: ellipsis;
text-decoration: none;
white-space: initial;
word-break: break-all;
&:hover {
text-decoration: underline;
}
}
}
&__total {
border-top: 1px dashed #c3c4c7;
margin-top: 2px;
&-heading {
font-size: 14px;
> :first-child {
text-transform: uppercase;
}
}
&-row {
font-size: 12px;
&--success {
color: #4ab866;
}
&--failed {
color: #b32d2e;
}
}
}
&__trigger {
font-size: 11px;
line-height: 1.4;
.vrts-test-run-trigger {
font-size: inherit;
margin-left: 5px;
margin-right: 5px;
}
&-notes {
color: #757575;
}
}
&__footer {
text-transform: uppercase;
text-align: center;
font-weight: 700;
}
}
@@ -0,0 +1,113 @@
<?php
use Vrts\Core\Utilities\Date_Time_Helpers;
use Vrts\Core\Utilities\Url_Helpers;
use Vrts\Models\Test_Run;
$trigger_note = Test_Run::get_trigger_note( $data['run'] );
?>
<div class="vrts-test-run-receipt">
<?php if ( $data['alerts'] ) : ?>
<a id="vrts-alert-receipt" class="vrts-test-run-receipt__link" data-vrts-alert="receipt" href="<?php echo esc_url( Url_Helpers::get_alert_page( 'receipt', $data['run']->id ) ); ?>"></a>
<?php endif; ?>
<div class="vrts-test-run-receipt__header">
<div class="vrts-test-run-receipt__header-logo">
<?php vrts()->logo(); ?>
<?php esc_html_e( 'VRTs Plugin', 'visual-regression-tests' ); ?>
</div>
<div class="vrts-test-run-receipt__header-info">
<?php esc_html_e( 'Test Receipt', 'visual-regression-tests' ); ?>
<a href="<?php echo esc_url( home_url( '/' ) ); ?>"><?php echo esc_html( home_url( '/' ) ); ?></a>
</div>
</div>
<div class="vrts-test-run-receipt__info">
<?php
// translators: %s: the run number.
printf( esc_html__( 'Run #%s', 'visual-regression-tests' ), esc_html( $data['run']->id ) );
?>
<?php echo wp_kses( Date_Time_Helpers::get_formatted_relative_date_time( $data['run']->finished_at ), [ 'time' => [ 'datetime' => true ] ] ); ?>
</div>
<div class="vrts-test-run-receipt__pages-status">
<div class="vrts-test-run-receipt__pages-status-heading">
<span><?php esc_html_e( 'Page', 'visual-regression-tests' ); ?></span>
<span><?php esc_html_e( 'Difference', 'visual-regression-tests' ); ?></span>
</div>
<?php
foreach ( $data['tests'] as $test ) :
$alert = array_values( array_filter( $data['alerts'], static function ( $alert ) use ( $test ) {
return $alert->post_id === $test['post_id'];
} ) );
$difference = $alert ? ceil( $alert[0]->differences ) : 0;
?>
<div class="vrts-test-run-receipt__pages-status-row">
<?php if ( $test['permalink'] ) : ?>
<a href="<?php echo esc_url( $test['permalink'] ); ?>"><?php echo esc_html( Url_Helpers::make_relative( $test['permalink'] ) ); ?></a>
<?php else : ?>
N/A
<?php endif; ?>
<span>
<?php
printf(
/* translators: %s. Test run receipt diff in pixels */
esc_html_x( '%spx', 'test run receipt difference', 'visual-regression-tests' ),
esc_html( number_format_i18n( $difference ) )
);
?>
</span>
</div>
<?php endforeach; ?>
</div>
<div class="vrts-test-run-receipt__total">
<div class="vrts-test-run-receipt__total-heading">
<span><?php esc_html_e( 'Total', 'visual-regression-tests' ); ?></span>
<span>
<?php
printf(
/* translators: %s. Number of tests */
esc_html( _n( '%s Test', '%s Tests', count( $data['tests'] ), 'visual-regression-tests' ) ), count( $data['tests'] )
);
?>
</span>
</div>
<div class="vrts-test-run-receipt__total-row vrts-test-run-receipt__total-row--success">
<span><?php esc_html_e( 'Passed', 'visual-regression-tests' ); ?></span>
<span>
<?php
$passed_tests_count = count( $data['tests'] ) - count( $data['alerts'] );
printf(
/* translators: %s. Number of tests */
esc_html( _n( '%s Test', '%s Tests', $passed_tests_count, 'visual-regression-tests' ) ), esc_html( $passed_tests_count )
);
?>
</span>
</div>
<div class="vrts-test-run-receipt__total-row vrts-test-run-receipt__total-row--failed">
<span><?php esc_html_e( 'Changes detected', 'visual-regression-tests' ); ?></span>
<span>
<?php
$changed_detected_count = count( $data['alerts'] );
printf(
/* translators: %s. Number of tests */
esc_html( _n( '%s Test', '%s Tests', $changed_detected_count, 'visual-regression-tests' ) ), esc_html( $changed_detected_count )
);
?>
</span>
</div>
</div>
<div class="vrts-test-run-receipt__trigger">
<?php esc_html_e( 'Trigger', 'visual-regression-tests' ); ?>
<span class="vrts-test-run-trigger vrts-test-run-trigger--<?php echo esc_attr( $data['run']->trigger ); ?>">
<?php echo esc_html( Test_Run::get_trigger_title( $data['run'] ) ); ?>
</span>
<?php if ( ! empty( $trigger_note ) ) : ?>
<span class="vrts-test-run-receipt__trigger-notes" title="<?php echo esc_attr( $trigger_note ); ?>">
<?php echo esc_html( $trigger_note ); ?>
</span>
<?php endif; ?>
</div>
<div class="vrts-test-run-receipt__footer">
<?php esc_html_e( 'Test Completed!', 'visual-regression-tests' ); ?>
</div>
</div>
@@ -0,0 +1,60 @@
@import "@wordpress/base-styles/colors.native"; // stylelint-disable-line scss/at-import-partial-extension
.vrts-test-run-success {
display: block;
padding: max(50px, 8%) 20px;
border-top: 0;
overflow: hidden;
&__inner {
margin-bottom: 1.5rem;
max-width: 600px;
margin-inline: auto;
}
&__lottie-player {
position: relative;
pointer-events: none;
display: flex;
align-items: center;
justify-content: center;
aspect-ratio: 10 / 8;
margin-top: calc(100% / (10 / 8) * 0.3 * -1);
svg {
position: absolute;
inset: 0;
}
}
&__content {
text-align: center;
margin-top: calc(100% / (10 / 8) * 0.3 * -1);
> p {
font-size: 16px;
font-weight: 600;
margin: 0;
}
blockquote {
margin: 2rem auto 0 auto;
max-width: 350px;
text-wrap: balance;
p,
cite {
color: $gray-50;
margin: 5px;
font-size: 1rem;
line-height: 1.5;
letter-spacing: 0.01em;
}
cite {
color: $gray-700;
font-size: 0.875rem;
}
}
}
}
@@ -0,0 +1,171 @@
<?php
$quotes = [
[
'quote' => __( 'Change is inevitable except from a vending machine.', 'visual-regression-tests' ),
'author' => 'Robert C. Gallagher',
],
[
'quote' => __( 'Its not what happens to you, but how you react to it that matters.', 'visual-regression-tests' ),
'author' => 'Epictetus',
],
[
'quote' => __( 'Every success story is a tale of constant adaptation, revision, and change.', 'visual-regression-tests' ),
'author' => 'Richard Branson',
],
[
'quote' => __( 'Change is hard at first, messy in the middle and gorgeous at the end.', 'visual-regression-tests' ),
'author' => 'Robin Sharma',
],
[
'quote' => __( 'Without deviation from the norm, progress is not possible.', 'visual-regression-tests' ),
'author' => 'Frank Zappa',
],
[
'quote' => __( 'Perfection is not attainable, but if we chase perfection we can catch excellence.', 'visual-regression-tests' ),
'author' => 'Vince Lombardi',
],
[
'quote' => __( 'Its the little details that are vital. Little things make big things happen.', 'visual-regression-tests' ),
'author' => 'John Wooden',
],
[
'quote' => __( 'It is not the strongest of the species that survive, nor the most intelligent. It is the one that is most adaptable to change.', 'visual-regression-tests' ),
'author' => 'Charles Darwin',
],
[
'quote' => __( 'Attention to detail is not about perfection. Its about excellence, about constant improvement.', 'visual-regression-tests' ),
'author' => 'Chris Denny',
],
[
'quote' => __( 'The difference between something good and something great is attention to detail.', 'visual-regression-tests' ),
'author' => 'Charles R Swindoll',
],
[
'quote' => __( 'Details make perfection, and perfection is not a detail.', 'visual-regression-tests' ),
'author' => 'Leonardo da Vinci',
],
[
'quote' => __( 'Not everything that is faced can be changed, but nothing can be changed until it is faced.', 'visual-regression-tests' ),
'author' => 'James Baldwin',
],
[
'quote' => __( 'Small deeds done are better than great deeds planned.', 'visual-regression-tests' ),
'author' => 'Peter Marshall',
],
[
'quote' => __( 'Great things are not done by impulse, but by a series of small things brought together.', 'visual-regression-tests' ),
'author' => 'Vincent Van Gogh',
],
[
'quote' => __( 'Success is the sum of small efforts, repeated day in and day out.', 'visual-regression-tests' ),
'author' => 'Robert Collier',
],
[
'quote' => __( 'To improve is to change; to be perfect is to change often.', 'visual-regression-tests' ),
'author' => 'Winston Churchill',
],
[
'quote' => __( 'The details are not the details. They make the design.', 'visual-regression-tests' ),
'author' => 'Charles Eames',
],
[
'quote' => __( 'God is in the details.', 'visual-regression-tests' ),
'author' => 'Ludwig Mies van der Rohe',
],
[
'quote' => __( 'Excellence is the gradual result of always striving to do better.', 'visual-regression-tests' ),
'author' => 'Pat Riley',
],
[
'quote' => __( 'The world is changed by your example, not by your opinion.', 'visual-regression-tests' ),
'author' => 'Paulo Coelho',
],
[
'quote' => __( 'Doing the little things can make a big difference.', 'visual-regression-tests' ),
'author' => 'Yogi Berra',
],
[
'quote' => __( 'Tiny tweaks can lead to big changes.', 'visual-regression-tests' ),
'author' => 'Amy Cuddy',
],
[
'quote' => __( 'Life is a series of natural and spontaneous changes. Dont resist them—that only creates sorrow. Let reality be reality.', 'visual-regression-tests' ),
'author' => 'Lao Tzu',
],
[
'quote' => __( 'A bend in the road is not the end of the road… unless you fail to make the turn.', 'visual-regression-tests' ),
'author' => 'Helen Keller',
],
[
'quote' => __( 'The only way to make sense out of change is to plunge into it, move with it, and join the dance.', 'visual-regression-tests' ),
'author' => 'Alan Watts',
],
[
'quote' => __( 'Change is the only constant in life.', 'visual-regression-tests' ),
'author' => 'Heraclitus',
],
[
'quote' => __( 'The art of life lies in a constant readjustment to our surroundings.', 'visual-regression-tests' ),
'author' => 'Okakura Kakuzō',
],
[
'quote' => __( 'Adaptability is about the powerful difference between adapting to cope and adapting to win.', 'visual-regression-tests' ),
'author' => 'Max McKeown',
],
[
'quote' => __( 'The pessimist complains about the wind; the optimist expects it to change; the realist adjusts the sails.', 'visual-regression-tests' ),
'author' => 'William Arthur Ward',
],
[
'quote' => __( 'The world hates change, yet it is the only thing that has brought progress.', 'visual-regression-tests' ),
'author' => 'Charles Kettering',
],
[
'quote' => __( 'Change is inevitable. Change is constant.', 'visual-regression-tests' ),
'author' => 'Benjamin Disraeli',
],
[
'quote' => __( 'You must welcome change as the rule but not as your ruler.', 'visual-regression-tests' ),
'author' => 'Denis Waitley',
],
[
'quote' => __( 'Change is the process by which the future invades our lives.', 'visual-regression-tests' ),
'author' => 'Alvin Toffler',
],
[
'quote' => __( 'Dont fear failure. Fear being in the exact same place next year as you are today.', 'visual-regression-tests' ),
'author' => 'Michael Hyatt',
],
[
'quote' => __( 'Sometimes good things fall apart so better things can fall together.', 'visual-regression-tests' ),
'author' => 'Marilyn Monroe',
],
[
'quote' => __( 'Change starts when someone sees the next step.', 'visual-regression-tests' ),
'author' => 'William Drayton',
],
];
mt_srand( $data['run']->id ); // phpcs:ignore WordPress.WP.AlternativeFunctions.rand_seeding_mt_srand -- It's ok.
$random_quote = $quotes[ array_rand( $quotes ) ];
?>
<vrts-test-run-success class="vrts-test-run-success postbox">
<div class="vrts-test-run-success__inner">
<div class="vrts-test-run-success__lottie-player" vrts-lottie-player></div>
<div class="vrts-test-run-success__content">
<?php if ( $data['is_receipt'] ) : ?>
<p><?php esc_html_e( 'Nice work no more Alerts left to review!', 'visual-regression-tests' ); ?></p>
<blockquote>
<p>"<?php echo esc_html( $random_quote['quote'] ); ?>"</p>
<cite>— <?php echo esc_html( $random_quote['author'] ); ?></cite>
</blockquote>
<?php else : ?>
<p><?php esc_html_e( 'Smooth sailing no changes found!', 'visual-regression-tests' ); ?></p>
<p><?php esc_html_e( "You're good to go.", 'visual-regression-tests' ); ?></p>
<?php endif; ?>
</div>
</div>
<span class="vrts-gradient-loader"></span>
</vrts-test-run-success>
@@ -0,0 +1,32 @@
import lottie from 'lottie-web/build/player/lottie_light';
class VrtsTestRunSuccess extends window.HTMLElement {
constructor() {
super();
this.resolveElements();
}
resolveElements() {
this.$lottiePlayer = this.querySelector( '[vrts-lottie-player]' );
}
connectedCallback() {
this.lottieAnimation = lottie.loadAnimation( {
path: `${ window.vrts_admin_vars.pluginUrl }/assets/animations/success-check.json`,
container: this.$lottiePlayer,
renderer: 'svg',
loop: false,
autoplay: true,
} );
}
disconnectedCallback() {
this.lottieAnimation.destroy();
}
loaAnimation() {
this.$lottiePlayer.play();
}
}
window.customElements.define( 'vrts-test-run-success', VrtsTestRunSuccess );
@@ -0,0 +1,183 @@
@import "@wordpress/base-styles/colors.native"; // stylelint-disable-line scss/at-import-partial-extension
@import "@wordpress/base-styles/breakpoints";
.vrts-test-runs-page {
display: block;
.tablenav.top {
clear: none;
}
}
.vrts-test-runs-list-table {
position: relative;
overflow: hidden;
.check-column {
text-align: center;
padding: 10px 0 0 10px !important;
width: 28px;
}
.column-status {
@media screen and (max-width: 782px) {
padding-bottom: 8px !important;
}
}
.vrts-runs-status {
&--passed {
color: $alert-green;
}
&--scheduled {
color: #f97b06;
}
&--has-alerts {
color: $alert-red;
}
&--running {
animation: vrts-rotate 1.5s linear infinite;
color: $blue-50;
}
}
.row-title {
display: flex;
align-items: center;
gap: 0.3125rem;
color: $blue-50;
line-height: 1;
padding: 3px 0;
&:where(a)::before {
content: "";
position: absolute;
top: 0;
left: -38px;
width: 100vw;
height: 100%;
}
}
[data-vrts-test-run-status="running"] .row-title {
color: #f97b06;
}
[data-vrts-test-run-status="scheduled"] .row-title {
color: $gray-70;
}
.row-actions {
display: flex;
flex-wrap: wrap;
gap: 4px;
visibility: hidden;
> *:not(:first-child)::before {
content: " | ";
}
}
tr {
&[data-has-alerts] {
.row-title,
.vrts-test-run-trigger,
.vrts-testing-status-wrapper > *:first-child {
font-weight: 600;
}
.row-title::after {
content: "";
width: 8px;
height: 8px;
background: var(--vrts-admin-theme-color);
border-radius: 50%;
flex: 0 0 auto;
}
}
&.test-run-highlighted {
animation: vrts-color-highlight 2s ease;
}
&:hover {
.row-actions {
visibility: visible;
}
}
}
.vrts-test-run-view-alerts {
display: inline-flex;
align-items: center;
gap: 4px;
}
}
.vrts-test-runs-list-queue-table {
margin-top: 10px;
margin-bottom: 20px;
thead,
tfoot {
display: none;
}
.subsubsub:has(+ &) {
float: none;
text-align: left;
}
}
// .vrts-test-run-details {
// display: none;
// [data-vrts-test-run-details="visible"] & {
// display: block;
// }
// .vrts-test-run-details-section {
// margin: 15px 0;
// &:first-child {
// margin-top: 25px;
// }
// &-title {
// border-bottom: 1px solid $gray-5;
// color: $alert-red;
// display: flex;
// flex-wrap: wrap;
// font-weight: 600;
// gap: 10px;
// justify-content: space-between;
// margin-bottom: 10px;
// padding-bottom: 10px;
// }
// &--passed {
// .vrts-test-run-details-section-title {
// color: $alert-green;
// }
// }
// ul {
// margin: 0;
// margin-left: 1rem;
// list-style: outside;
// }
// }
// .button {
// margin-bottom: 4px;
// }
// }
@@ -0,0 +1,45 @@
<?php
use Vrts\Features\Admin_Notices;
use Vrts\Services\Manual_Test_Service;
?>
<vrts-test-runs-page class="wrap vrts-list-table-page vrts-test-runs-page">
<h1 class="wp-heading-inline">
<?php esc_html_e( 'Runs', 'visual-regression-tests' ); ?>
</h1>
<hr class="wp-header-end">
<?php
$list_table = $data['list_queue_table'];
$list_table->prepare_items();
$list_table->views();
$list_table->display();
?>
<form method="post">
<?php
$list_table = $data['list_table'];
$list_table->prepare_items();
$list_table->views();
$list_table->display();
if ( $list_table->has_items() ) {
$list_table->inline_edit();
}
?>
</form>
<?php
$vrts_manual_test_service = new Manual_Test_Service();
$test_status = $vrts_manual_test_service->get_option();
if ( $test_status ) {
$vrts_manual_test_service->delete_option();
if ( '1' === $test_status ) {
Admin_Notices::render_notification( 'test_started', false, [] );
} elseif ( '2' === $test_status ) {
Admin_Notices::render_notification( 'test_failed', false, [] );
}
}
?>
</vrts-test-runs-page>
@@ -0,0 +1,53 @@
class VrtsTestRunsPage extends window.HTMLElement {
constructor() {
super();
this.resolveElements();
}
resolveElements() {
this.$runsListTable = this.querySelector(
'form .vrts-test-runs-list-table'
);
}
connectedCallback() {
this.highlightNewTestRuns();
}
highlightNewTestRuns() {
const testRunIds = new Set(
JSON.parse(
window.localStorage.getItem( 'vrtsNewTestRuns' ) || '[]'
)
);
const rows = this.$runsListTable.querySelectorAll(
'tr[data-test-run-id]'
);
let staggerTimeout = 0;
rows.forEach( ( row ) => {
const testRunId = row.getAttribute( 'data-test-run-id' );
if ( row.getAttribute( 'data-test-run-new' ) === 'true' ) {
if ( ! testRunIds.has( testRunId ) ) {
testRunIds.add( testRunId );
setTimeout( () => {
row.classList.add( 'test-run-highlighted' );
}, staggerTimeout );
staggerTimeout += 200;
}
} else if ( testRunIds.has( testRunId ) ) {
testRunIds.delete( testRunId );
}
} );
window.localStorage.setItem(
'vrtsNewTestRuns',
JSON.stringify( [ ...testRunIds ] )
);
}
}
window.customElements.define( 'vrts-test-runs-page', VrtsTestRunsPage );
@@ -0,0 +1,93 @@
@import "@wordpress/base-styles/colors.native"; // stylelint-disable-line scss/at-import-partial-extension
@import "@wordpress/base-styles/breakpoints";
.page-title-actions {
list-style: none;
display: inline-flex;
padding: 0 0 4px;
margin: 0;
gap: 1ch;
li {
margin: 0;
}
.vrts-page-title-action {
position: relative;
top: -3px;
min-height: 32px;
line-height: 2.30769231;
padding: 0 12px;
}
}
.vrts_tests_page_wp_link {
@media (max-width: $break-mobile) {
margin-top: -250px !important;
max-height: 80vh;
}
.link-url-field,
.link-search-field {
width: 100% !important;
max-width: 100% !important;
margin: 0 !important;
}
.link-search-wrapper {
position: relative;
.spinner {
position: absolute;
right: 40px;
top: 4px;
margin: 0 !important;
}
}
.vrts-tests--active {
pointer-events: none;
opacity: 0.6;
}
#link-options {
display: none;
visibility: hidden;
}
#wplink-link-existing-content {
padding-top: 8px;
}
#wp-link .query-results {
top: 80px;
max-height: calc(100% - 90px);
height: 100%;
}
.item {
&-permalink {
font-size: 0.66rem;
}
}
}
.vrts-list-table-page {
.quick-edit-row-test {
label {
pointer-events: none;
}
textarea {
width: 100%;
}
.inline-edit-col {
max-width: 485px;
}
}
}
@@ -0,0 +1,11 @@
<?php
switch ( $data['action'] ) {
default:
$template = __DIR__ . '/views/tests-page-list.php';
break;
}
if ( file_exists( $template ) ) {
include $template;
}
@@ -0,0 +1,333 @@
/* global jQuery, ajaxurl, inlineEditTest */
document
.getElementById( 'show-modal-add-new' )
?.addEventListener( 'click', () => {
window.wpLink.open( 'input_hidden_internal_url' );
} );
window.wp = window.wp || {};
( function ( $, wp ) {
window.inlineEditTest = {
/**
* Initializes the inline and bulk post editor.
*
* Binds event handlers to the Escape key to close the inline editor
* and to the save and close buttons. Changes DOM to be ready for inline
* editing. Adds event handler to bulk edit.
*
* @since 2.7.0
*
* @memberof inlineEditTest
*
* @return {void}
*/
init() {
const t = this,
qeRow = $( '#inline-edit' );
t.type = 'test';
// Test ID prefix.
t.what = '#test-';
/**
* Binds the Escape key to revert the changes and close the quick editor.
*
* @return {boolean} The result of revert.
*/
qeRow.on( 'keyup', function ( e ) {
// Revert changes if Escape key is pressed.
if ( e.which === 27 ) {
return inlineEditTest.revert();
}
} );
/**
* Reverts changes and close the quick editor if the cancel button is clicked.
*
* @return {boolean} The result of revert.
*/
$( '.cancel', qeRow ).on( 'click', function () {
return inlineEditTest.revert();
} );
/**
* Saves changes in the quick editor if the save(named: update) button is clicked.
*
* @return {boolean} The result of save.
*/
$( '.save', qeRow ).on( 'click', function () {
return inlineEditTest.save( this );
} );
/**
* If Enter is pressed, and the target is not the cancel button, save the post.
*
* @return {boolean} The result of save.
*/
$( 'td', qeRow ).on( 'keydown', function ( e ) {
if (
e.which === 13 &&
! e.shiftKey &&
! $( e.target ).hasClass( 'cancel' )
) {
return inlineEditTest.save( this );
}
} );
/**
* Binds click event to the .editinline button which opens the quick editor.
*/
$( '#the-list' ).on( 'click', '.editinline', function () {
$( this ).attr( 'aria-expanded', 'true' );
inlineEditTest.edit( this );
} );
/**
* Adds onclick events to the apply buttons.
*/
$( '#doaction' ).on( 'click', function ( e ) {
t.whichBulkButtonId = $( this ).attr( 'id' );
const n = t.whichBulkButtonId.substr( 2 );
if ( 'edit' === $( 'select[name="' + n + '"]' ).val() ) {
e.preventDefault();
} else if (
$( 'form#posts-filter tr.inline-editor' ).length > 0
) {
t.revert();
}
} );
},
/**
* Toggles the quick edit window, hiding it when it's active and showing it when
* inactive.
*
* @since 2.7.0
*
* @memberof inlineEditTest
*
* @param {Object} el Element within a post table row.
*/
toggle( el ) {
const t = this;
// eslint-disable-next-line
$( t.what + t.getId( el ) ).css( 'display' ) === 'none'
? t.revert()
: t.edit( el );
},
/**
* Creates a quick edit window for the post that has been clicked.
*
* @since 2.7.0
*
* @memberof inlineEditTest
*
* @param {number|Object} id The ID of the clicked post or an element within a post
* table row.
* @return {boolean} Always returns false at the end of execution.
*/
edit( id ) {
const t = this;
t.revert();
if ( typeof id === 'object' ) {
id = t.getId( id );
}
// Add the new edit row with an extra blank row underneath to maintain zebra striping.
const editRow = $( '#inline-edit' ).clone( true );
$( 'td', editRow ).attr(
'colspan',
$( 'th:visible, td:visible', '.widefat:first thead' ).length
);
// Remove the ID from the copied row and let the `for` attribute reference the hidden ID.
$( 'td', editRow ).find( '#quick-edit-legend' ).removeAttr( 'id' );
$( 'td', editRow )
.find( 'p[id^="quick-edit-"]' )
.removeAttr( 'id' );
$( t.what + id )
.removeClass( 'is-expanded' )
.hide()
.after( editRow )
.after( '<tr class="hidden"></tr>' );
// Populate fields in the quick edit window.
const rowData = $( '#inline_' + id );
const hideInputSelectorsText = $(
'.hide_css_selectors',
rowData
).text();
$( ':input[name="hide_css_selectors"]', editRow ).val(
hideInputSelectorsText
);
$( editRow )
.attr( 'id', 'edit-' + id )
.addClass( 'inline-editor' )
.show();
$( ':input[name="hide_css_selectors"]', editRow ).trigger(
'focus'
);
return false;
},
/**
* Saves the changes made in the quick edit window to the post.
* Ajax saving is only for Quick Edit and not for bulk edit.
*
* @since 2.7.0
*
* @param {number} id The ID for the post that has been changed.
* @return {boolean} False, so the form does not submit when pressing
* Enter on a focused field.
*/
save( id ) {
if ( typeof id === 'object' ) {
id = this.getId( id );
}
$( 'table.widefat .spinner' ).addClass( 'is-active' );
const params = {
action: 'vrts_test_quick_edit_save',
test_id: id,
hide_css_selectors: $(
'#edit-' + id + ' [name="hide_css_selectors"]'
).val(),
nonce: $( '#_vrts_test_quick_edit_nonce' ).val(),
};
// Make Ajax request
$.post(
ajaxurl,
params,
function ( response ) {
$( 'table.widefat .spinner' ).removeClass( 'is-active' );
// Work with the response.
response = $.parseJSON( response );
if ( ! response.success ) {
const $errorNotice = $(
'#edit-' + id + ' .inline-edit-save .notice-error'
);
$errorNotice.removeClass( 'hidden' );
$errorNotice.text( response.message );
wp.a11y.speak( response.message );
return;
}
$( '#inline_' + id + ' .hide_css_selectors' ).text(
response.hide_css_selectors
);
// Hide the quick edit window.
const $tableWideFat = $( '.widefat' );
id = $( '.inline-editor', $tableWideFat ).attr( 'id' );
if ( id ) {
$( '.spinner', $tableWideFat ).removeClass(
'is-active'
);
// Remove both the inline-editor and its hidden tr siblings.
$( '#' + id )
.siblings( 'tr.hidden' )
.addBack()
.remove();
id = id.substr( id.lastIndexOf( '-' ) + 1 );
// Show the post row and move focus back to the Quick Edit button.
$( '#test-' + id )
.fadeIn( 400 )
.find( '.editinline' )
.attr( 'aria-expanded', 'false' )
.trigger( 'focus' );
}
wp.a11y.speak( response.message );
// Update the snapshot column in the table and set the status of the snapshot.
const snapshotStatus = response.snapshot_status;
const $snapshotColumn = $(
'#test-' + id + ' .base_screenshot_date'
);
if (
null !== snapshotStatus &&
undefined !== snapshotStatus &&
'' !== snapshotStatus
) {
$snapshotColumn.text( snapshotStatus );
}
},
'html'
);
// Prevent submitting the form when pressing Enter on a focused field.
return false;
},
/**
* Hides and empties the Quick Edit and/or Bulk Edit windows.
*
* @since 2.7.0
*
* @memberof inlineEditTest
*
* @return {boolean} Always returns false.
*/
revert() {
const $tableWideFat = $( '.widefat' );
let id = $( '.inline-editor', $tableWideFat ).attr( 'id' );
if ( id ) {
$( '.spinner', $tableWideFat ).removeClass( 'is-active' );
// Remove both the inline-editor and its hidden tr siblings.
$( '#' + id )
.siblings( 'tr.hidden' )
.addBack()
.remove();
id = id.substr( id.lastIndexOf( '-' ) + 1 );
// Show the post row and move focus back to the Quick Edit button.
$( this.what + id )
.show()
.find( '.editinline' )
.attr( 'aria-expanded', 'false' )
.trigger( 'focus' );
}
return false;
},
/**
* Gets the ID for a the post that you want to quick edit from the row in the quick
* edit table.
*
* @since 2.7.0
*
* @memberof inlineEditTest
*
* @param {Object} o DOM row object to get the ID for.
* @return {string} The post ID extracted from the table row in the object.
*/
getId( o ) {
const id = $( o ).closest( 'tr' ).attr( 'id' ),
parts = id.split( '-' );
return parts[ parts.length - 1 ];
},
};
$( function () {
if ( $( '.vrts-list-table-page' ).length ) {
inlineEditTest.init();
}
} );
} )( jQuery, window.wp );
@@ -0,0 +1,134 @@
<?php
use Vrts\Features\Admin_Notices;
use Vrts\Features\Subscription;
use Vrts\Services\Manual_Test_Service;
?>
<div class="wrap vrts-list-table-page">
<h1 class="wp-heading-inline">
<?php esc_html_e( 'Tests', 'visual-regression-tests' ); ?>
</h1>
<menu class="page-title-actions">
<li>
<button type="button" class="vrts-page-title-action button-primary"
id="<?php echo ( ! $data['is_connected'] || intval( $data['remaining_tests'] ) === 0 ) ? 'modal-add-new-disabled' : 'show-modal-add-new'; ?>"
<?php echo ( ! $data['is_connected'] || intval( $data['remaining_tests'] ) === 0 ) ? ' disabled' : ''; ?>>
<?php esc_html_e( 'Add New', 'visual-regression-tests' ); ?>
</button>
</li>
<?php if ( Subscription::get_subscription_status() ) : ?>
<li>
<form method="post" id="form-run-manual-tests">
<?php wp_nonce_field( 'submit_run_manual_tests', '_wpnonce' ); ?>
<input type="submit" name="submit_run_manual_tests" value="<?php esc_attr_e( 'Run All Tests', 'visual-regression-tests' ); ?>"
class="vrts-page-title-action button-secondary"
id="<?php echo ( ! $data['is_connected'] || ! $data['running_tests_count'] ) ? 'run-manual-tests-disabled' : 'run-manual-tests'; ?>"
<?php echo ( ! $data['is_connected'] || ! $data['running_tests_count'] ) ? ' disabled' : ''; ?>
>
</form>
</li>
<?php endif; ?>
</menu>
<?php if ( isset( $data['search_query'] ) && '' !== $data['search_query'] ) { ?>
<span class="subtitle">
<?php
printf(
/* translators: %s: search query. */
esc_html__( 'Search results for: %s', 'visual-regression-tests' ),
'<strong>' . esc_html( $data['search_query'] ) . '</strong>'
);
?>
</span>
<?php } ?>
<hr class="wp-header-end">
<form method="post">
<input type="hidden" name="page" value="vrts-tests_list_table">
<?php
$list_table = $data['list_table'];
$list_table->prepare_items();
$list_table->views();
$list_table->search_box( esc_attr__( 'Search', 'visual-regression-tests' ), 'search_id' );
$list_table->display();
if ( $list_table->has_items() ) {
$list_table->inline_edit();
}
$vrts_manual_test_service = new Manual_Test_Service();
$test_status = $vrts_manual_test_service->get_option();
if ( $test_status ) {
$vrts_manual_test_service->delete_option();
if ( '1' === $test_status ) {
Admin_Notices::render_notification( 'test_started', false, [] );
} elseif ( '2' === $test_status ) {
Admin_Notices::render_notification( 'test_failed', false, [] );
}
}
?>
</form>
</div>
<div id="wp-link-backdrop" style="display: none"></div>
<div id="wp-link-wrap" class="wp-core-ui vrts_tests_page_wp_link" style="display: none" role="dialog" aria-labelledby="link-modal-title">
<form id="wp-link" tabindex="-1" method="post">
<?php wp_nonce_field( 'internal-linking', '_ajax_linking_nonce', false ); ?>
<?php wp_nonce_field( 'vrts_page_tests_nonce' ); ?>
<h1 id="link-modal-title"><?php esc_html_e( 'Add New Test', 'visual-regression-tests' ); ?></h1>
<button type="button" id="wp-link-close"><span class="screen-reader-text"><?php esc_html_e( 'Close', 'visual-regression-tests' ); ?></span></button>
<div id="link-selector">
<div id="link-options">
<p class="howto" id="wplink-enter-url"><?php esc_html_e( 'Destination URL', 'visual-regression-tests' ); ?></p>
<div>
<input id="wp-link-url" class="link-url-field" type="text" aria-describedby="wplink-enter-url" name="internal_url" readonly />
<input id="wp-link-id" type="hidden" name="post_id" />
</div>
</div>
<p class="howto" id="wplink-link-existing-content"><?php esc_html_e( 'Search', 'visual-regression-tests' ); ?></p>
<div id="search-panel">
<div class="link-search-wrapper">
<input type="search" id="wp-link-search" class="link-search-field" autocomplete="off" aria-describedby="wplink-link-existing-content" />
<span class="spinner"></span>
</div>
<div id="search-results" class="query-results" tabindex="0">
<ul></ul>
<div class="river-waiting">
<span class="spinner"></span>
</div>
</div>
<div id="most-recent-results" class="query-results" tabindex="0">
<div class="query-notice" id="query-notice-message">
<em class="query-notice-default"><?php esc_html_e( 'No search term specified. Showing recent items.', 'visual-regression-tests' ); ?></em>
<em class="query-notice-hint screen-reader-text"><?php esc_html_e( 'Search or use up and down arrow keys to select an item.', 'visual-regression-tests' ); ?></em>
</div>
<ul></ul>
<div class="river-waiting">
<span class="spinner"></span>
</div>
</div>
</div>
</div>
<div class="submitbox">
<div id="wp-link-cancel">
<button type="button" class="button"><?php esc_html_e( 'Cancel', 'visual-regression-tests' ); ?></button>
</div>
<div id="wp-link-update">
<?php
submit_button(
__( 'Add New Test', 'visual-regression-tests' ),
'button button-primary',
'submit_add_new_test',
false,
[ 'id' => 'wp-link-submit' ]
);
?>
</div>
</div>
</form>
</div>
@@ -0,0 +1,6 @@
.vrts_upgrade_page {
iframe {
margin-top: 10px;
}
}
@@ -0,0 +1,9 @@
<div class="wrap vrts_upgrade_page">
<iframe
src="https://vrts.app/pricing-embedded/?current-tier=<?php echo rawurlencode( esc_attr( $data['tier_id'] ) ); ?>"
title="<?php echo esc_html( $data['title'] ); ?>"
loading="lazy"
width="100%"
id="vrts_upgrade_iframe">
</iframe>
</div>
@@ -0,0 +1,13 @@
import iFrameResize from 'iframe-resizer/js/iframeResizer';
const isVrtsUpgradePage = document.querySelector( '.vrts_upgrade_page' );
if ( isVrtsUpgradePage ) {
iFrameResize(
{
checkOrigin: false,
heightCalculationMethod: 'taggedElement',
},
'#vrts_upgrade_iframe'
);
}