Move admin tester folders to root

This commit is contained in:
Paul Sealock 2022-06-07 15:18:37 +12:00
parent 54c847abb6
commit 6ded9053b6
145 changed files with 3556 additions and 0 deletions

View File

@ -0,0 +1,67 @@
<?php
use Automattic\WooCommerce\Admin\Notes\Note;
register_woocommerce_admin_test_helper_rest_route(
'/admin-notes/add-note/v1',
'admin_notes_add_note'
);
function admin_notes_add_note( $request ) {
$note = new Note();
$mock_note_data = get_mock_note_data();
$type = $request->get_param( 'type' );
$layout = $request->get_param( 'layout' );
$note->set_name( $request->get_param( 'name' ) );
$note->set_title( $request->get_param( 'title' ) );
$note->set_content( $mock_note_data[ 'content' ] );
$note->set_image( $mock_note_data[ $type ][ $layout ] );
$note->set_layout( $layout );
$note->set_type( $type );
possibly_add_action( $note );
if ( 'email' === $type ) {
add_email_note_params( $note );
}
$note->save();
return true;
}
function add_email_note_params( $note ) {
$additional_data = array(
'role' => 'administrator',
);
$note->set_content_data( (object) $additional_data );
}
function possibly_add_action( $note ) {
if ( $note->get_type() === 'info' ) {
return;
}
$action_name = sprintf(
'test-action-%s',
$note->get_name()
);
$note->add_action( $action_name, 'Test action', wc_admin_url() );
}
function get_mock_note_data() {
$plugin_url = site_url() . '/wp-content/plugins/woocommerce-admin-test-helper/';
return array(
'content' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud.',
'info' => array(
'banner' => $plugin_url . 'images/admin-notes/banner.jpg',
'thumbnail' => $plugin_url . 'images/admin-notes/thumbnail.jpg',
'plain' => ''
),
'email' => array(
'plain' => $plugin_url . 'images/admin-notes/woocommerce-logo-vector.png'
),
'update' => array(
'plain' => ''
)
);
}

View File

@ -0,0 +1,18 @@
<?php
register_woocommerce_admin_test_helper_rest_route(
'/admin-notes/delete-all-notes/v1',
'admin_notes_delete_all_notes'
);
function admin_notes_delete_all_notes() {
global $wpdb;
$deleted_note_count = $wpdb->query( "DELETE FROM {$wpdb->prefix}wc_admin_notes" );
$deleted_action_count = $wpdb->query( "DELETE FROM {$wpdb->prefix}wc_admin_note_actions" );
return array(
'deleted_note_count' => $deleted_note_count,
'deleted_action_count' => $deleted_action_count,
);
}

View File

@ -0,0 +1,41 @@
<?php
function register_woocommerce_admin_test_helper_rest_route( $route, $callback, $additional_options = array() ) {
add_action( 'rest_api_init', function() use ( $route, $callback, $additional_options ) {
$default_options = array(
'methods' => 'POST',
'callback' => $callback,
'permission_callback' => function( $request ) {
if ( ! wc_rest_check_manager_permissions( 'settings', 'edit' ) ) {
return new \WP_Error(
'woocommerce_rest_cannot_edit',
__( 'Sorry, you cannot perform this action', 'woocommerce-admin-test-helper' )
);
}
return true;
},
);
$default_options = array_merge( $default_options, $additional_options );
register_rest_route(
'wc-admin-test-helper',
$route,
$default_options
);
} );
}
require( 'admin-notes/delete-all-notes.php' );
require( 'admin-notes/add-note.php' );
require( 'tools/trigger-wca-install.php' );
require( 'tools/trigger-cron-job.php' );
require( 'tools/run-wc-admin-daily.php' );
require( 'options/rest-api.php' );
require( 'tools/delete-all-products.php');
require( 'tools/disable-wc-email.php' );
require( 'tools/trigger-update-callbacks.php' );
require( 'tracks/tracks-debug-log.php' );
require( 'features/features.php' );
require( 'rest-api-filters/rest-api-filters.php' );
require( 'rest-api-filters/hook.php' );

View File

@ -0,0 +1,59 @@
<?php
use Automattic\WooCommerce\Admin\Features\Features;
const OPTION_NAME_PREFIX = 'wc_admin_helper_feature_values';
register_woocommerce_admin_test_helper_rest_route(
'/features/(?P<feature_name>[a-z0-9_\-]+)/toggle',
'toggle_feature',
array(
'methods' => 'POST',
)
);
register_woocommerce_admin_test_helper_rest_route(
'/features',
'get_features',
array(
'methods' => 'GET',
)
);
register_woocommerce_admin_test_helper_rest_route(
'/features/reset',
'reset_features',
array(
'methods' => 'POST',
)
);
function toggle_feature( $request ) {
$features = get_features();
$custom_feature_values = get_option( OPTION_NAME_PREFIX, array() );
$feature_name = $request->get_param( 'feature_name' );
if ( ! isset( $features[$feature_name ]) ) {
return new WP_REST_Response( $features, 204 );
}
if ( isset( $custom_feature_values[$feature_name] ) ) {
unset( $custom_feature_values[$feature_name] );
} else {
$custom_feature_values[$feature_name] = ! $features[ $feature_name ];
}
update_option(OPTION_NAME_PREFIX, $custom_feature_values );
return new WP_REST_Response( get_features(), 200 );
}
function reset_features() {
delete_option( OPTION_NAME_PREFIX );
return new WP_REST_Response( get_features(), 200 );
}
function get_features() {
if ( function_exists( 'wc_admin_get_feature_config' ) ) {
return apply_filters( 'woocommerce_admin_get_feature_config', wc_admin_get_feature_config() );
}
return array();
}

View File

@ -0,0 +1,79 @@
<?php
register_woocommerce_admin_test_helper_rest_route(
'/options',
'wca_test_helper_get_options',
array(
'methods' => 'GET',
'args' => array(
'page' => array(
'description' => 'Current page of the collection.',
'type' => 'integer',
'default' => 1,
'sanitize_callback' => 'absint',
),
'per_page' => array(
'description' => 'Maximum number of items to be returned in result set.',
'type' => 'integer',
'default' => 10,
'sanitize_callback' => 'absint',
),
'search' => array(
'description' => 'Limit results to those matching a string.',
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
),
),
)
);
register_woocommerce_admin_test_helper_rest_route(
'/options/(?P<option_names>(.*)+)',
'wca_test_helper_delete_option',
array(
'methods' => 'DELETE',
'args' => array(
'option_names' => array(
'type' => 'string',
),
),
)
);
function wca_test_helper_delete_option( $request ) {
global $wpdb;
$option_names = explode( ',', $request->get_param( 'option_names' ) );
$option_names = array_map( function( $option_name ) {
return "'" . $option_name . "'";
}, $option_names );
$option_names = implode( ',', $option_names );
$query = "delete from {$wpdb->prefix}options where option_name in ({$option_names})";
$wpdb->query( $query );
return new WP_REST_RESPONSE( null, 204 );
}
function wca_test_helper_get_options( $request ) {
global $wpdb;
$per_page = $request->get_param( 'per_page' );
$page = $request->get_param( 'page' );
$search = $request->get_param( 'search' );
$query = "
select option_id, option_name, option_value, autoload
from {$wpdb->prefix}options
";
if ( $search ) {
$query .= "where option_name like '%{$search}%'";
}
$query .= ' order by option_id desc limit 30';
$options = $wpdb->get_results( $query );
return new WP_REST_Response( $options, 200 );
}

View File

@ -0,0 +1,52 @@
<?php
$filters = get_option(WCA_Test_Helper_Rest_Api_Filters::WC_ADMIN_TEST_HELPER_REST_API_FILTER_OPTION, [] );
function array_dot_set( &$array, $key, $value ) {
if ( is_null( $key ) ) {
return $array = $value;
}
$keys = explode('.', $key);
while ( count($keys) > 1 ) {
$key = array_shift($keys);
if (! isset($array[$key]) || ! is_array($array[$key]) ) {
$array[$key] = [];
}
$array = &$array[$key];
}
$array[ array_shift($keys) ] = $value;
return $array;
}
add_filter(
'rest_request_after_callbacks',
function ( $response, array $handler, \WP_REST_Request $request ) use ( $filters ) {
if (! $response instanceof \WP_REST_Response ) {
return $response;
}
$route = $request->get_route();
$filters = array_filter(
$filters, function ( $filter ) use ( $request, $route ) {
if ($filter['enabled'] && $filter['endpoint'] == $route ) {
return true;
}
return false;
}
);
$data = $response->get_data();
foreach ( $filters as $filter ) {
array_dot_set($data, $filter['dot_notation'], $filter['replacement']);
}
$response->set_data($data);
return $response;
},
10,
3
);

View File

@ -0,0 +1,109 @@
<?php
register_woocommerce_admin_test_helper_rest_route(
'/rest-api-filters',
[ WCA_Test_Helper_Rest_Api_Filters::class, 'create' ],
array(
'methods' => 'POST',
'args' => array(
'endpoint' => array(
'description' => 'Rest API endpoint.',
'type' => 'string',
'required' => true,
'sanitize_callback' => 'sanitize_text_field',
),
'dot_notation' => array(
'description' => 'Dot notation of the target field.',
'type' => 'string',
'required' => true,
'sanitize_callback' => 'sanitize_text_field',
),
'replacement' => array(
'description' => 'Replacement value for the target field.',
'type' => 'string',
'required' => true,
'sanitize_callback' => 'sanitize_text_field',
),
),
)
);
register_woocommerce_admin_test_helper_rest_route(
'/rest-api-filters',
[ WCA_Test_Helper_Rest_Api_Filters::class, 'delete' ],
array(
'methods' => 'DELETE',
'args' => array(
'index' => array(
'description' => 'Rest API endpoint.',
'type' => 'integer',
'required' => true,
),
),
)
);
register_woocommerce_admin_test_helper_rest_route(
'/rest-api-filters/(?P<index>\d+)/toggle',
[ WCA_Test_Helper_Rest_Api_Filters::class, 'toggle' ],
array(
'methods' => 'POST',
)
);
class WCA_Test_Helper_Rest_Api_Filters {
const WC_ADMIN_TEST_HELPER_REST_API_FILTER_OPTION = 'wc-admin-test-helper-rest-api-filters';
public static function create( $request ) {
$endpoint = $request->get_param('endpoint');
$dot_notation = $request->get_param('dot_notation');
$replacement = $request->get_param('replacement');
self::update(
function ( $filters ) use (
$endpoint,
$dot_notation,
$replacement
) {
$filters[] = array(
'endpoint' => $endpoint,
'dot_notation' => $dot_notation,
'replacement' => filter_var( $replacement, FILTER_VALIDATE_BOOLEAN ),
'enabled' => true,
);
return $filters;
}
);
return new WP_REST_RESPONSE(null, 204);
}
public static function update( callable $callback ) {
$filters = get_option(self::WC_ADMIN_TEST_HELPER_REST_API_FILTER_OPTION, array());
$filters = $callback( $filters );
return update_option(self::WC_ADMIN_TEST_HELPER_REST_API_FILTER_OPTION, $filters);
}
public static function delete( $request ) {
self::update(
function ( $filters ) use ( $request ) {
array_splice($filters, $request->get_param('index'), 1);
return $filters;
}
);
return new WP_REST_RESPONSE(null, 204);
}
public static function toggle( $request ) {
self::update(
function ( $filters ) use ( $request ) {
$index = $request->get_param('index');
$filters[$index]['enabled'] = !$filters[$index]['enabled'];
return $filters;
}
);
return new WP_REST_RESPONSE(null, 204);
}
}

View File

@ -0,0 +1,16 @@
<?php
register_woocommerce_admin_test_helper_rest_route(
'/tools/delete-all-products/v1',
'tools_delete_all_products'
);
function tools_delete_all_products() {
$query = new \WC_Product_Query();
$products = $query->get_products();
foreach ( $products as $product ) {
$product->delete( true );
}
return true;
}

View File

@ -0,0 +1,46 @@
<?php
register_woocommerce_admin_test_helper_rest_route(
'/tools/toggle-emails/v1',
'toggle_emails'
);
register_woocommerce_admin_test_helper_rest_route(
'/tools/get-email-status/v1',
'get_email_status',
array(
'methods' => 'GET',
)
);
function toggle_emails() {
$emails_disabled = 'yes';
if ( $emails_disabled === get_option( 'wc_admin_test_helper_email_disabled', 'no' ) ) {
$emails_disabled = 'no';
remove_filter('woocommerce_email_get_option', 'disable_wc_emails' );
}
update_option('wc_admin_test_helper_email_disabled', $emails_disabled );
return new WP_REST_Response( $emails_disabled, 200 );
}
function get_email_status() {
$emails_disabled = get_option( 'wc_admin_test_helper_email_disabled', 'no' );
return new WP_REST_Response( $emails_disabled, 200 );
}
if ( 'yes' === get_option( 'wc_admin_test_helper_email_disabled', 'no' ) ) {
add_filter('woocommerce_email_get_option', 'disable_wc_emails' );
add_action( 'woocommerce_email', 'unhook_other_wc_emails' );
}
function disable_wc_emails( $key ) {
if ( $key === 'enabled' ) {
return false;
}
}
function unhook_other_wc_emails( $email ) {
remove_action( 'woocommerce_low_stock_notification', array( $email, 'low_stock' ) );
remove_action( 'woocommerce_no_stock_notification', array( $email, 'no_stock' ) );
remove_action( 'woocommerce_product_on_backorder_notification', array( $email, 'backorder' ) );
remove_action( 'woocommerce_new_customer_note_notification', array( $email->emails['WC_Email_Customer_Note'], 'trigger' ) );
}

View File

@ -0,0 +1,11 @@
<?php
register_woocommerce_admin_test_helper_rest_route(
'/tools/run-wc-admin-daily/v1',
'tools_run_wc_admin_daily'
);
function tools_run_wc_admin_daily() {
do_action('wc_admin_daily');
return true;
}

View File

@ -0,0 +1,98 @@
<?php
register_woocommerce_admin_test_helper_rest_route(
'/tools/get-cron-list/v1',
'tools_get_cron_list',
array(
'methods' => 'GET',
)
);
register_woocommerce_admin_test_helper_rest_route(
'/tools/trigger-selected-cron/v1',
'trigger_selected_cron',
array(
'methods' => 'POST',
'args' => array(
'hook' => array(
'description' => 'Name of the cron that will be triggered.',
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
),
'signature' => array(
'description' => 'Signature of the cron to trigger.',
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
),
),
)
);
function tools_get_cron_list() {
$crons = _get_cron_array();
$events = array();
if ( empty( $crons ) ) {
return array();
}
foreach ( $crons as $cron ) {
foreach ( $cron as $hook => $data ) {
foreach ( $data as $signature => $element ) {
$events[ $hook ] = (object) array(
'hook' => $hook,
'signature' => $signature,
);
}
}
}
return new WP_REST_Response( $events, 200 );
}
function trigger_selected_cron( $request ) {
$hook = $request->get_param( 'hook' );
$signature = $request->get_param( 'signature' );
if ( ! isset( $hook ) || ! isset( $signature ) ) {
return;
}
$crons = _get_cron_array();
foreach ( $crons as $cron ) {
if ( isset( $cron[ $hook ][ $signature ] ) ) {
$args = $cron[ $hook ][ $signature ]['args'];
delete_transient( 'doing_cron' );
$scheduled = schedule_event( $hook, $args );
if ( false === $scheduled ) {
return $scheduled;
}
add_filter( 'cron_request', function( array $cron_request ) {
$cron_request['url'] = add_query_arg( 'run-cron', 1, $cron_request['url'] );
return $cron_request;
} );
spawn_cron();
sleep( 1 );
return true;
}
}
return false;
}
function schedule_event( $hook, $args = array() ) {
$event = (object) array(
'hook' => $hook,
'timestamp' => 1,
'schedule' => false,
'args' => $args,
);
$crons = (array) _get_cron_array();
$key = md5( serialize( $event->args ) );
$crons[ $event->timestamp ][ $event->hook ][ $key ] = array(
'schedule' => $event->schedule,
'args' => $event->args,
);
uksort( $crons, 'strnatcasecmp' );
return _set_cron_array( $crons );
}

View File

@ -0,0 +1,47 @@
<?php
use Automattic\WooCommerce\Admin\API\Reports\Cache;
register_woocommerce_admin_test_helper_rest_route(
'/tools/get-update-versions/v1',
'tools_get_wc_admin_versions',
array(
'methods' => 'GET',
)
);
register_woocommerce_admin_test_helper_rest_route(
'/tools/trigger-selected-update-callbacks/v1',
'trigger_selected_update_callbacks',
array(
'methods' => 'POST',
'args' => array(
'version' => array(
'description' => 'Name of the update version',
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
)
),
)
);
function tools_get_wc_admin_versions() {
$db_updates = \WC_Install::get_db_update_callbacks();
return new WP_REST_Response( array_keys( $db_updates ), 200 );
}
function trigger_selected_update_callbacks( $request ) {
$version = $request->get_param( 'version' );
if ( ! isset( $version ) ) {
return;
}
$db_updates = \WC_Install::get_db_update_callbacks();
$update_callbacks = $db_updates[ $version ];
foreach ( $update_callbacks as $update_callback ) {
call_user_func( $update_callback );
}
return false;
}

View File

@ -0,0 +1,11 @@
<?php
register_woocommerce_admin_test_helper_rest_route(
'/tools/trigger-wca-install/v1',
'tools_trigger_wca_install'
);
function tools_trigger_wca_install() {
\WC_Install::install();
return true;
}

View File

@ -0,0 +1,53 @@
<?php
/**
* A class for logging tracked events.
*/
class TracksDebugLog {
/**
* Logger class to use.
*
* @var WC_Logger_Interface|null
*/
private $logger;
/**
* Logger source.
*
* @var string logger source.
*/
private $source = 'tracks';
/**
* Initialize hooks.
*/
public function __construct() {
add_filter( 'woocommerce_tracks_event_properties', array( $this, 'log_event' ), 10, 2 );
$logger = wc_get_logger();
$this->logger = $logger;
$this->logger = $logger;
}
/**
* Log the event.
*
* @param array $properties Event properties.
* @param string $event_name Event name.
*/
public function log_event( $properties, $event_name ) {
$this->logger->debug(
$event_name,
array( 'source' => $this->source )
);
foreach ( $properties as $key => $property ) {
$this->logger->debug(
" - {$key}: {$property}",
array( 'source' => $this->source )
);
}
return $properties;
}
}
new TracksDebugLog();

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -0,0 +1,136 @@
/**
* External dependencies.
*/
import { useState } from '@wordpress/element';
import { Button, SelectControl } from '@wordpress/components';
import apiFetch from '@wordpress/api-fetch';
export const AddNote = () => {
const [ isAdding, setIsAdding ] = useState( false );
const [ hasAdded, setHasAdded ] = useState( false );
const [ errorMessage, setErrorMessage ] = useState( false );
const [ noteType, setNoteType ] = useState( 'info' );
const [ noteLayout, setNoteLayout ] = useState( 'plain' );
async function triggerAddNote() {
setIsAdding( true );
setHasAdded( false );
setErrorMessage( false );
const name = prompt( 'Enter the note name' );
if ( ! name ) {
setIsAdding( false );
return;
}
const title = prompt( 'Enter the note title' );
if ( ! title ) {
setIsAdding( false );
return;
}
try {
await apiFetch( {
path: '/wc-admin-test-helper/admin-notes/add-note/v1',
method: 'POST',
data: {
name,
type: noteType,
layout: noteLayout,
title,
},
} );
setHasAdded( true );
}
catch ( ex ) {
setErrorMessage( ex.message );
}
setIsAdding( false );
}
function onTypeChange( val ) {
setNoteType( val );
if ( val !== 'info' ) {
setNoteLayout( 'plain' );
}
}
function onLayoutChange( val ) {
setNoteLayout( val );
}
function getAddNoteDescription() {
switch ( noteType ){
case 'email':
return (
<>
This will add a new <strong>email</strong> note. Enable email insights{' '}
<a href="/wp-admin/admin.php?page=wc-settings&tab=email">
here
</a>{' '}
and run the cron to send the note by email.
</>);
default:
return (
<>
This will add a new note. Currently only the note name
and title will be used to create the note.
</>
);
}
}
return (
<>
<p><strong>Add a note</strong></p>
<div>
{ getAddNoteDescription() }
<br/>
<div className="woocommerce-admin-test-helper__add-notes">
<Button
onClick={ triggerAddNote }
disabled={ isAdding }
isPrimary
>
Add admin note
</Button>
<SelectControl
label="Type"
onChange={ onTypeChange }
labelPosition="side"
options={ [
{ label: 'Info', value: 'info' },
{ label: 'Update', value: 'update' },
{ label: 'Email', value: 'email' },
] }
value={ noteType }
/>
<SelectControl
label="Layout"
onChange={ onLayoutChange }
labelPosition="side"
options={ [
{ label: 'Plain', value: 'plain' },
{ label: 'Banner', value: 'banner' },
{ label: 'Thumbnail', value: 'thumbnail' },
] }
disabled={ noteType !== 'info' }
value={ noteLayout }
/>
</div>
<br/>
<span className="woocommerce-admin-test-helper__action-status">
{ isAdding && 'Adding, please wait' }
{ hasAdded && 'Note added' }
{ errorMessage && (
<>
<strong>Error:</strong> { errorMessage }
</>
) }
</span>
</div>
</>
);
};

View File

@ -0,0 +1,16 @@
/**
* Internal dependencies.
*/
import { DeleteAllNotes } from './delete-all-notes';
import { AddNote } from './add-note';
export const AdminNotes = () => {
return (
<>
<h2>Admin notes</h2>
<p>This section contains tools for managing admin notes.</p>
<AddNote/>
<DeleteAllNotes/>
</>
);
};

View File

@ -0,0 +1,69 @@
/**
* External dependencies.
*/
import { useState } from '@wordpress/element';
import { Button } from '@wordpress/components';
import apiFetch from '@wordpress/api-fetch';
export const DeleteAllNotes = () => {
const [ isDeleting, setIsDeleting ] = useState( false );
const [ deleteStatus, setDeleteStatus ] = useState( false );
const [ errorMessage, setErrorMessage ] = useState( false );
async function triggerDeleteAllNotes() {
setIsDeleting( true );
setErrorMessage( false );
setDeleteStatus( false );
try {
const response = await apiFetch( {
path: '/wc-admin-test-helper/admin-notes/delete-all-notes/v1',
method: 'POST',
} );
setDeleteStatus( response );
} catch ( ex ) {
setErrorMessage( ex.message );
}
setIsDeleting( false );
}
return (
<>
<p><strong>Delete all admin notes</strong></p>
<p>
This will delete all notes from the <code>wp_wc_admin_notes</code>
table, and actions from the <code>wp_wc_admin_note_actions</code>
table.
<br/>
<Button
onClick={ triggerDeleteAllNotes }
disabled={ isDeleting }
isPrimary
>
Delete all notes
</Button>
<br/>
<span className="woocommerce-admin-test-helper__action-status">
{ isDeleting && 'Deleting, please wait.' }
{ deleteStatus && (
<>
Deleted{ ' ' }
<strong>{ deleteStatus.deleted_note_count }</strong>{ ' ' }
admin notes and{ ' ' }
<strong>{ deleteStatus.deleted_action_count }</strong>{ ' ' }
actions.
</>
) }
{ errorMessage && (
<>
<strong>Error: </strong>
{ errorMessage }
</>
) }
</span>
</p>
</>
);
};

View File

@ -0,0 +1 @@
export { AdminNotes } from './admin-notes.js';

View File

@ -0,0 +1,72 @@
/**
* External dependencies
*/
import { TabPanel } from '@wordpress/components';
import { applyFilters } from '@wordpress/hooks';
/**
* Internal dependencies
*/
import { AdminNotes } from '../admin-notes';
import { default as Tools } from '../tools';
import { default as Options } from '../options';
import { default as Experiments } from '../experiments';
import { default as Features } from '../features';
import { default as RestAPIFilters } from '../rest-api-filters';
const tabs = applyFilters( 'woocommerce_admin_test_helper_tabs', [
{
name: 'options',
title: 'Options',
content: <Options />,
},
{
name: 'admin-notes',
title: 'Admin notes',
content: <AdminNotes />,
},
{
name: 'tools',
title: 'Tools',
content: <Tools />,
},
{
name: 'experiments',
title: 'Experiments',
content: <Experiments />,
},
{
name: 'features',
title: 'Features',
content: <Features />,
},
{
name: 'rest-api-filters',
title: 'REST API FIlters',
content: <RestAPIFilters />,
},
] );
export function App() {
return (
<div className="wrap">
<h1>WooCommerce Admin Test Helper</h1>
<TabPanel
className="woocommerce-admin-test-helper__main-tab-panel"
activeClass="active-tab"
tabs={ tabs }
initialTabName={ tabs[ 0 ].name }
>
{ ( tab ) => (
<>
{ tab.content }
{ applyFilters(
`woocommerce_admin_test_helper_tab_${ tab.name }`,
[]
) }
</>
) }
</TabPanel>
</div>
);
}

View File

@ -0,0 +1 @@
export { App } from './app';

View File

@ -0,0 +1,56 @@
/**
* External dependencies
*/
import { withDispatch } from '@wordpress/data';
import { compose } from '@wordpress/compose';
import { Button } from '@wordpress/components';
import { useState } from '@wordpress/element';
/**
* Internal dependencies
*/
import { STORE_KEY } from './data/constants';
import './data';
function NewExperimentForm( { addExperiment } ) {
const [ experimentName, setExperimentName ] = useState( null );
const [ variation, setVariation ] = useState( 'treatment' );
const getInputValue = ( event ) => {
setExperimentName( event.target.value );
};
const getVariationInput = ( event ) => {
setVariation( event.target.value );
};
const AddNewExperiment = () => {
addExperiment( experimentName, variation );
};
return (
<div className="manual-input">
<div className="description">
Don&apos;t see an experiment you want to test? Add it manually.
</div>
<input type="text" onChange={ getInputValue } />
<select value={ variation } onChange={ getVariationInput }>
<option value="treatment">treatment</option>
<option value="control">control</option>
</select>
<Button isPrimary onClick={ AddNewExperiment }>
Add
</Button>
</div>
);
}
export default compose(
withDispatch( ( dispatch ) => {
const { addExperiment } = dispatch( STORE_KEY );
return {
addExperiment,
};
} )
)( NewExperimentForm );

View File

@ -0,0 +1,8 @@
const TYPES = {
TOGGLE_EXPERIMENT: 'TOGGLE_EXPERIMENT',
SET_EXPERIMENTS: 'SET_EXPERIMENTS',
ADD_EXPERIMENT: 'ADD_EXPERIMENT',
DELETE_EXPERIMENT: 'DELETE_EXPERIMENT',
};
export default TYPES;

View File

@ -0,0 +1,105 @@
/**
* External dependencies
*/
import { apiFetch } from '@wordpress/data-controls';
/**
* Internal dependencies
*/
import TYPES from './action-types';
import {
EXPERIMENT_NAME_PREFIX,
TRANSIENT_NAME_PREFIX,
TRANSIENT_TIMEOUT_NAME_PREFIX,
} from './constants';
function toggleFrontendExperiment( experimentName, newVariation ) {
let storageItem = JSON.parse(
window.localStorage.getItem( EXPERIMENT_NAME_PREFIX + experimentName )
);
// If the experiment is not in localStorage, consider it as a new.
if ( storageItem === null ) {
storageItem = {
experimentName,
retrievedTimestamp: Date.now(),
};
}
storageItem.variationName = newVariation;
storageItem.ttl = 3600;
window.localStorage.setItem(
EXPERIMENT_NAME_PREFIX + experimentName,
JSON.stringify( storageItem )
);
}
function* toggleBackendExperiment( experimentName, newVariation ) {
try {
const payload = {};
payload[ TRANSIENT_NAME_PREFIX + experimentName ] = newVariation;
payload[ TRANSIENT_TIMEOUT_NAME_PREFIX + experimentName ] =
Math.round( Date.now() / 1000 ) + 3600;
yield apiFetch( {
method: 'POST',
path: '/wc-admin/options',
headers: { 'content-type': 'application/json' },
body: JSON.stringify( payload ),
} );
} catch ( error ) {
throw new Error();
}
}
export function* toggleExperiment( experimentName, currentVariation ) {
const newVariation =
currentVariation === 'control' ? 'treatment' : 'control';
toggleFrontendExperiment( experimentName, newVariation );
yield toggleBackendExperiment( experimentName, newVariation );
return {
type: TYPES.TOGGLE_EXPERIMENT,
experimentName,
newVariation,
};
}
export function setExperiments( experiments ) {
return {
type: TYPES.SET_EXPERIMENTS,
experiments,
};
}
export function* addExperiment( experimentName, variation ) {
toggleFrontendExperiment( experimentName, variation );
yield toggleBackendExperiment( experimentName, variation );
return {
type: TYPES.ADD_EXPERIMENT,
experimentName,
variation,
};
}
export function* deleteExperiment( experimentName ) {
window.localStorage.removeItem( EXPERIMENT_NAME_PREFIX + experimentName );
const optionNames = [
TRANSIENT_NAME_PREFIX + experimentName,
TRANSIENT_TIMEOUT_NAME_PREFIX + experimentName,
];
yield apiFetch( {
method: 'DELETE',
path: '/wc-admin-test-helper/options/' + optionNames.join( ',' ),
} );
return {
type: TYPES.DELETE_EXPERIMENT,
experimentName,
};
}

View File

@ -0,0 +1,6 @@
export const STORE_KEY = 'wc-admin-helper/experiments';
export const EXPERIMENT_NAME_PREFIX = 'explat-experiment--';
export const TRANSIENT_NAME_PREFIX = '_transient_abtest_variation_';
export const TRANSIENT_TIMEOUT_NAME_PREFIX =
'_transient_timeout_abtest_variation';
export const API_NAMESPACE = '/wc-admin-test-helper';

View File

@ -0,0 +1,72 @@
/**
* Internal dependencies
*/
import TYPES from './action-types';
const DEFAULT_STATE = {
experiments: [],
};
const reducer = ( state = DEFAULT_STATE, action ) => {
switch ( action.type ) {
case TYPES.DELETE_EXPERIMENT:
return {
...state,
experiments: state.experiments.filter( ( experiment ) => {
return experiment.name !== action.experimentName;
} ),
};
case TYPES.ADD_EXPERIMENT:
const existingExperimentIndex = state.experiments.findIndex(
( element ) => {
return element.name === action.experimentName;
}
);
const newExperiment = {
name: action.experimentName,
variation: action.variation,
};
const newExperiments =
existingExperimentIndex !== -1
? state.experiments
.slice( 0, existingExperimentIndex )
.concat( newExperiment )
.concat(
state.experiments.slice(
existingExperimentIndex + 1
)
)
: [
...state.experiments,
{
name: action.experimentName,
variation: action.variation,
},
];
return {
...state,
experiments: newExperiments,
};
case TYPES.TOGGLE_EXPERIMENT:
return {
...state,
experiments: state.experiments.map( ( experiment ) => ( {
...experiment,
variation:
experiment.name === action.experimentName
? action.newVariation
: experiment.variation,
} ) ),
};
case TYPES.SET_EXPERIMENTS:
return {
...state,
experiments: action.experiments,
};
default:
return state;
}
};
export default reducer;

View File

@ -0,0 +1,68 @@
/**
* External dependencies
*/
import { apiFetch } from '@wordpress/data-controls';
/**
* Internal dependencies
*/
import { setExperiments } from './actions';
import {
EXPERIMENT_NAME_PREFIX,
TRANSIENT_NAME_PREFIX,
API_NAMESPACE,
} from './constants';
function getExperimentsFromFrontend() {
const storageItems = Object.entries( { ...window.localStorage } ).filter(
( item ) => {
return item[ 0 ].indexOf( EXPERIMENT_NAME_PREFIX ) === 0;
}
);
return storageItems.map( ( storageItem ) => {
const [ key, value ] = storageItem;
const objectValue = JSON.parse( value );
return {
name: key.replace( EXPERIMENT_NAME_PREFIX, '' ),
variation: objectValue.variationName || 'control',
};
} );
}
export function* getExperiments() {
try {
const response = yield apiFetch( {
path: `${ API_NAMESPACE }/options?search=_transient_abtest_variation_`,
} );
const experimentsFromBackend = response.map( ( experiment ) => {
return {
name: experiment.option_name.replace(
TRANSIENT_NAME_PREFIX,
''
),
variation:
experiment.option_value === 'control'
? 'control'
: 'treatment',
};
} );
// Remove duplicate.
const experiments = getExperimentsFromFrontend()
.concat( experimentsFromBackend )
.filter(
( value, index, self ) =>
index ===
self.findIndex(
( t ) =>
t.place === value.place && t.name === value.name
)
);
yield setExperiments( experiments );
} catch ( error ) {
throw new Error();
}
}

View File

@ -0,0 +1,3 @@
export function getExperiments( state ) {
return state.experiments;
}

View File

@ -0,0 +1,110 @@
/**
* External dependencies
*/
import { withDispatch, withSelect } from '@wordpress/data';
import { compose } from '@wordpress/compose';
import { Button } from '@wordpress/components';
import { OPTIONS_STORE_NAME } from '@woocommerce/data';
/**
* Internal dependencies
*/
import { STORE_KEY } from './data/constants';
import './data';
import NewExperimentForm from './NewExperimentForm';
function Experiments( {
experiments,
toggleExperiment,
deleteExperiment,
isTrackingEnabled,
isResolving,
} ) {
if ( isResolving ) {
return null;
}
return (
<div id="wc-admin-test-helper-experiments">
<h2>Experiments</h2>
{ isTrackingEnabled === 'no' && (
<p className="tracking-disabled">
The following list might not be complete without tracking
enabled. <br />
Please visit&nbsp;
<a
target="_blank"
href={
wcSettings.adminUrl +
'/admin.php?page=wc-settings&tab=advanced&section=woocommerce_com'
}
rel="noreferrer"
>
WooCommerce &#8594; Settings &#8594; Advanced &#8594;
Woocommerce.com
</a>
&nbsp;and check{ ' ' }
<b>Allow usage of WooCommerce to be tracked</b>.
</p>
) }
<NewExperimentForm />
<table className="experiments wp-list-table striped table-view-list widefat">
<thead>
<tr>
<th>Experiment</th>
<th>Variation</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{ experiments.map( ( { name, variation }, index ) => {
return (
<tr key={ index }>
<td className="experiment-name">{ name }</td>
<td align="center">{ variation }</td>
<td className="actions" align="center">
<Button
onClick={ () => {
toggleExperiment( name, variation );
} }
isPrimary
>
Toggle
</Button>
<Button
onClick={ () => {
deleteExperiment( name );
} }
className="btn btn-danger"
>
Delete
</Button>
</td>
</tr>
);
} ) }
</tbody>
</table>
</div>
);
}
export default compose(
withSelect( ( select ) => {
const { getExperiments } = select( STORE_KEY );
const { getOption, isResolving } = select( OPTIONS_STORE_NAME );
return {
experiments: getExperiments(),
isTrackingEnabled: getOption( 'woocommerce_allow_tracking' ),
isResolving: isResolving( 'getOption', [ 'getExperiments' ] ),
};
} ),
withDispatch( ( dispatch ) => {
const { toggleExperiment, deleteExperiment } = dispatch( STORE_KEY );
return {
toggleExperiment,
deleteExperiment,
};
} )
)( Experiments );

View File

@ -0,0 +1,7 @@
const TYPES = {
TOGGLE_FEATURE: 'TOGGLE_FEATURE',
SET_FEATURES: 'SET_FEATURES',
SET_MODIFIED_FEATURES: 'SET_MODIFIED_FEATURES',
};
export default TYPES;

View File

@ -0,0 +1,57 @@
/**
* External dependencies
*/
import { apiFetch } from '@wordpress/data-controls';
import { controls } from '@wordpress/data';
/**
* Internal dependencies
*/
import TYPES from './action-types';
import { API_NAMESPACE, STORE_KEY } from './constants';
export function* resetModifiedFeatures() {
try {
const response = yield apiFetch( {
path: `${ API_NAMESPACE }/features/reset`,
method: 'POST',
} );
yield setModifiedFeatures( [] );
yield setFeatures( response );
} catch ( error ) {
throw new Error();
}
}
export function* toggleFeature( featureName ) {
try {
const response = yield apiFetch( {
method: 'POST',
path: API_NAMESPACE + '/features/' + featureName + '/toggle',
headers: { 'content-type': 'application/json' },
} );
yield setFeatures( response );
yield controls.dispatch(
STORE_KEY,
'invalidateResolutionForStoreSelector',
'getModifiedFeatures'
);
} catch ( error ) {
throw new Error();
}
}
export function setFeatures( features ) {
return {
type: TYPES.SET_FEATURES,
features,
};
}
export function setModifiedFeatures( modifiedFeatures ) {
return {
type: TYPES.SET_MODIFIED_FEATURES,
modifiedFeatures,
};
}

View File

@ -0,0 +1,3 @@
export const STORE_KEY = 'wc-admin-helper/features';
export const OPTION_NAME_PREFIX = 'wc_admin_helper_feature_values';
export const API_NAMESPACE = '/wc-admin-test-helper';

View File

@ -0,0 +1,22 @@
/**
* External dependencies
*/
import { registerStore } from '@wordpress/data';
import { controls } from '@wordpress/data-controls';
/**
* Internal dependencies
*/
import * as actions from './actions';
import * as resolvers from './resolvers';
import * as selectors from './selectors';
import reducer from './reducer';
import { STORE_KEY } from './constants';
export default registerStore( STORE_KEY, {
actions,
selectors,
resolvers,
controls,
reducer,
} );

View File

@ -0,0 +1,28 @@
/**
* Internal dependencies
*/
import TYPES from './action-types';
const DEFAULT_STATE = {
features: {},
modifiedFeatures: [],
};
const reducer = ( state = DEFAULT_STATE, action ) => {
switch ( action.type ) {
case TYPES.SET_MODIFIED_FEATURES:
return {
...state,
modifiedFeatures: action.modifiedFeatures,
};
case TYPES.SET_FEATURES:
return {
...state,
features: action.features,
};
default:
return state;
}
};
export default reducer;

View File

@ -0,0 +1,38 @@
/**
* External dependencies
*/
import { apiFetch } from '@wordpress/data-controls';
/**
* Internal dependencies
*/
import { setFeatures, setModifiedFeatures } from './actions';
import { API_NAMESPACE, OPTION_NAME_PREFIX } from './constants';
export function* getModifiedFeatures() {
try {
const response = yield apiFetch( {
path: `wc-admin/options?options=` + OPTION_NAME_PREFIX,
} );
yield setModifiedFeatures(
response && response[ OPTION_NAME_PREFIX ]
? Object.keys( response[ OPTION_NAME_PREFIX ] )
: []
);
} catch ( error ) {
throw new Error();
}
}
export function* getFeatures() {
try {
const response = yield apiFetch( {
path: `${ API_NAMESPACE }/features`,
} );
yield setFeatures( response );
} catch ( error ) {
throw new Error();
}
}

View File

@ -0,0 +1,7 @@
export function getFeatures( state ) {
return state.features;
}
export function getModifiedFeatures( state ) {
return state.modifiedFeatures;
}

View File

@ -0,0 +1,72 @@
/**
* External dependencies
*/
import { useDispatch, useSelect } from '@wordpress/data';
import { Button } from '@wordpress/components';
/**
* Internal dependencies
*/
import { STORE_KEY } from './data/constants';
import './data';
function Features() {
const { features = {}, modifiedFeatures = [] } = useSelect( ( select ) => {
const { getFeatures, getModifiedFeatures } = select( STORE_KEY );
return {
features: getFeatures(),
modifiedFeatures: getModifiedFeatures(),
};
} );
const { toggleFeature, resetModifiedFeatures } = useDispatch( STORE_KEY );
return (
<div id="wc-admin-test-helper-features">
<h2>
Features
<Button
disabled={ modifiedFeatures.length === 0 }
onClick={ () => resetModifiedFeatures() }
isSecondary
style={ { marginLeft: '24px' } }
>
Reset to defaults
</Button>
</h2>
<table className="features wp-list-table striped table-view-list widefat">
<thead>
<tr>
<th>Feature Name</th>
<th>Enabled?</th>
<th>Toggle</th>
</tr>
</thead>
<tbody>
{ Object.keys( features ).map( ( feature_name ) => {
return (
<tr key={ feature_name }>
<td className="feature-name">
{ feature_name }
</td>
<td>{ features[ feature_name ].toString() }</td>
<td>
<Button
onClick={ () => {
toggleFeature( feature_name );
} }
isPrimary
>
Toggle
</Button>
</td>
</tr>
);
} ) }
</tbody>
</table>
</div>
);
}
export default Features;

View File

@ -0,0 +1,18 @@
/**
* External dependencies
*/
import { render } from '@wordpress/element';
/**
* Internal dependencies
*/
import { App } from './app';
import './index.scss';
const appRoot = document.getElementById(
'woocommerce-admin-test-helper-app-root'
);
if (appRoot) {
render(<App />, appRoot);
}

Some files were not shown because too many files have changed in this diff Show More