WCA Test helper - add remote inbox notification staging importer (#48735)

* Add Remote inbox notifications management

* Remove admin notes section -- replaced by remote inbox notifications

* Add import from staging and production

* Add changefile(s) from automation for the following project(s): woocommerce-beta-tester

* WIP - change test to run

* Display failed rules on error

* Change run to test

* Add changefile(s) from automation for the following project(s): woocommerce-beta-tester, woocommerce

* Retire Remote Spec Ruel Validation -- use test action from Remote Inbox Notifications

* Run spec when all rules have passed

* Fix typo

* Change btn text to Run

* Update copy text

* Place delete all button on the left side

* Update plugins/woocommerce-beta-tester/api/remote-inbox-notifications/class-wca-test-helper-remote-inbox-notifications.php

Co-authored-by: Ilyas Foo <foo.ilyas@gmail.com>

* Update plugins/woocommerce/src/Admin/RemoteSpecs/RuleProcessors/RuleEvaluator.php

Co-authored-by: Ilyas Foo <foo.ilyas@gmail.com>

* Update plugins/woocommerce-beta-tester/api/remote-inbox-notifications/class-wca-test-helper-remote-inbox-notifications.php

Co-authored-by: Ilyas Foo <foo.ilyas@gmail.com>

* Add changefile(s) from automation for the following project(s): woocommerce-beta-tester

* Separate remote inbox notification heler functions into a class and fix DELETE method errors

* Fix typo

* Display all errors

* Fix errors

* Fix error with test

* Add import from URL

* Change func name to importFromUrl

* Rename 48735-feature-remote-notiifcation-importer to 48735-feature-remote-notification-importer

Fix filename typo

* Add changefile(s) from automation for the following project(s): woocommerce-beta-tester

* Fix changelog filename typo

Fix changelog filename typo

* Add changefile(s) from automation for the following project(s): woocommerce-beta-tester

* Revert adding new changelog

It seems the new CI is automatically creating a changelog based on branch name 43d6abe3e7

---------

Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Ilyas Foo <foo.ilyas@gmail.com>
This commit is contained in:
Moon 2024-06-24 23:32:36 -07:00 committed by GitHub
parent 655c1436ec
commit a09522df1c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 873 additions and 8 deletions

View File

@ -61,3 +61,4 @@ require 'live-branches/manifest.php';
require 'live-branches/install.php';
require 'tools/set-block-template-logging-threshold.php';
require 'remote-spec-validator/class-wca-test-helper-remote-spec-validator.php';
require 'remote-inbox-notifications/class-wca-test-helper-remote-inbox-notifications.php';

View File

@ -0,0 +1,140 @@
<?php
use Automattic\WooCommerce\Admin\RemoteInboxNotifications\SpecRunner;
use Automattic\WooCommerce\Admin\RemoteInboxNotifications\RemoteInboxNotificationsEngine;
use Automattic\WooCommerce\Admin\RemoteInboxNotifications\RemoteInboxNotificationsDataSourcePoller;
use Automattic\WooCommerce\Admin\RemoteSpecs\RuleProcessors\GetRuleProcessor;
use Automattic\WooCommerce\Admin\RemoteSpecs\RuleProcessors\RuleEvaluator;
/**
* Helper class for remote inbox notifications.
*/
class WC_Beta_Tester_Remote_Inbox_Notifications_Helper {
/**
* Get the name of the transient used to store the remote inbox notifications.
*
* @return string The transient name.
*/
public static function get_transient_name() {
return 'woocommerce_admin_' . RemoteInboxNotificationsDataSourcePoller::ID . '_specs';
}
/**
* Retrieve the transient data using the transient name.
*
* @return mixed The transient data.
*/
public static function get_transient() {
return get_transient( static::get_transient_name() );
}
/**
* Delete a specific notification by its ID from the database.
*
* @param int $id The ID of the notification to delete.
* @return bool True on success, false on failure.
*/
public static function delete_by_id( $id ) {
global $wpdb;
$wpdb->delete( $wpdb->prefix . 'wc_admin_notes', array( 'note_id' => $id ) );
$wpdb->delete( $wpdb->prefix . 'wc_admin_note_actions', array( 'note_id' => $id ) );
return true;
}
/**
* Delete all notifications and their associated actions from the database.
*
* @global wpdb $wpdb WordPress database abstraction object.
* @return array Associative array containing the count of deleted notes and actions.
*/
public static function delete_all() {
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,
);
}
/**
* Test a specific notification by its name and optionally run it.
*
* @param string $name The name of the notification to test.
* @param string|null $locale The locale of the notification. Defaults to the current system locale.
* @param bool $run Whether to run the notification if the test passes. Defaults to false.
* @return true|WP_Error True on success, WP_Error on failure.
*/
public static function test( $name, $locale = null, $run = false ) {
$notifications = static::get_transient();
if ( ! $locale ) {
$locale = get_locale();
}
if ( ! isset( $notifications[ $locale ][ $name ] ) ) {
return new WP_Error(
404,
"'{$name}' was not found in the latest remote inbox notification transient. Please re-run the import command.",
);
}
$spec = $notifications[ $locale ][ $name ];
$test = true;
$failed_rules = array();
$rule_processor = new GetRuleProcessor();
foreach ( $spec->rules as $rule ) {
if ( ! is_object( $rule ) ) {
$test = false;
break;
}
$processor = $rule_processor->get_processor( $rule->type );
$processor_result = $processor->process( $rule, null );
if ( ! $processor_result ) {
$test = false;
$failed_rules[] = $rule;
}
}
if ( $test && $run ) {
$stored_state = RemoteInboxNotificationsEngine::get_stored_state();
SpecRunner::run_spec( $spec, $stored_state );
}
if ( ! $test ) {
return new WP_Error(
400,
"Test failed for '{$name}'",
array( 'failed_rules' => $failed_rules )
);
}
return true;
}
/**
* Import specifications and store them in the transient.
*
* @param array $specs An array of specifications to import.
* @return bool True on success.
*/
public static function import( $specs ) {
$stored_state = RemoteInboxNotificationsEngine::get_stored_state();
$transient = static::get_transient();
foreach ( $specs as $spec ) {
SpecRunner::run_spec( $spec, $stored_state );
if ( isset( $spec->locales ) && is_array( $spec->locales ) ) {
foreach ( $spec->locales as $locale ) {
$transient[ $locale->locale ][ $spec->slug ] = $spec;
}
}
}
set_transient( static::get_transient_name(), $transient );
return true;
}
}

View File

@ -0,0 +1,195 @@
<?php
defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Admin\RemoteInboxNotifications\SpecRunner;
use Automattic\WooCommerce\Admin\RemoteInboxNotifications\RemoteInboxNotificationsEngine;
use Automattic\WooCommerce\Admin\RemoteInboxNotifications\RemoteInboxNotificationsDataSourcePoller;
use Automattic\WooCommerce\Admin\RemoteSpecs\RuleProcessors\GetRuleProcessor;
use Automattic\WooCommerce\Admin\RemoteSpecs\RuleProcessors\RuleEvaluator;
require_once dirname( __FILE__ ) . '/class-wc-beta-tester-remote-inbox-notifications-helper.php';
register_woocommerce_admin_test_helper_rest_route(
'/remote-inbox-notifications',
array( WCA_Test_Helper_Remote_Inbox_Notifications::class, 'get_items' ),
array(
'methods' => 'GET',
)
);
register_woocommerce_admin_test_helper_rest_route(
'/remote-inbox-notifications/(?P<id>\d+)/delete',
array( WCA_Test_Helper_Remote_Inbox_Notifications::class, 'delete' ),
array(
'methods' => 'POST',
'args' => array(
'id' => array(
'description' => 'Rest API endpoint.',
'type' => 'integer',
'required' => true,
),
),
)
);
register_woocommerce_admin_test_helper_rest_route(
'/remote-inbox-notifications/(?P<name>(.*)+)/test',
array( WCA_Test_Helper_Remote_Inbox_Notifications::class, 'test' ),
array(
'methods' => 'GET',
'args' => array(
'name' => array(
'description' => 'Note name.',
'type' => 'string',
'required' => true,
),
),
)
);
register_woocommerce_admin_test_helper_rest_route(
'/remote-inbox-notifications/delete-all',
array( WCA_Test_Helper_Remote_Inbox_Notifications::class, 'delete_all_items' ),
array(
'methods' => 'POST',
)
);
register_woocommerce_admin_test_helper_rest_route(
'/remote-inbox-notifications/import',
array( WCA_Test_Helper_Remote_Inbox_Notifications::class, 'import' ),
array(
'methods' => 'POST',
)
);
/**
* Class WCA_Test_Helper_Remote_Inbox_Notifications.
*/
class WCA_Test_Helper_Remote_Inbox_Notifications {
/**
* Delete a notification.
*
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response
*/
public static function delete( $request ) {
WC_Beta_Tester_Remote_Inbox_Notifications_Helper::delete_by_id( $request->get_param( 'id' ) );
return new WP_REST_Response(
array(
'success' => true,
'message' => 'Remote inbox notification deleted.',
),
200
);
}
/**
* Delete all notifications.
*
* @return WP_REST_Response
*/
public static function delete_all_items() {
$deleted = WC_Beta_Tester_Remote_Inbox_Notifications_Helper::delete_all();
return new WP_REST_Response( $deleted, 200 );
}
/**
* Return a list of class-based notes.
* These should be excluded from the list of notes to be displayed in the inbox as we don't have control over them.
*
* @return array
*/
private static function get_notes_to_exclude() {
$vars = array( 'other_note_classes', 'note_classes_to_added_or_updated' );
$note_names = array();
$reflection = new ReflectionClass( '\Automattic\WooCommerce\Internal\Admin\Events' );
foreach ( $vars as $var ) {
$property = $reflection->getProperty( $var );
$property->setAccessible( true );
$notes = $property->getValue();
$note_names = array_merge(
$note_names,
array_map(
function ( $note ) {
return $note::NOTE_NAME;
},
$notes
)
);
}
return $note_names;
}
/**
* Return all notifications.
*
* @return WP_REST_Response
*/
public static function get_items() {
global $wpdb;
$items = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}wc_admin_notes ORDER BY note_id desc", ARRAY_A );
$notes_added_via_classes = self::get_notes_to_exclude();
$items = array_filter(
$items,
function ( $item ) use ( $notes_added_via_classes ) {
return ! in_array( $item['name'], $notes_added_via_classes, true );
}
);
return new WP_REST_Response( array_values( $items ), 200 );
}
/**
* Test and run a remote inbox notification.
*
* @param WP_REST_Request $request The full request data.
*
* @return WP_REST_Response
*/
public static function test( $request ) {
$name = $request->get_param( 'name' );
$result = WC_Beta_Tester_Remote_Inbox_Notifications_Helper::test( $name, null, true );
if ( $result instanceof WP_Error ) {
$message = $result->get_error_data();
} else {
$message = $name . ': All rules passed successfully';
}
return new WP_REST_Response(
array(
'success' => true === $result,
'message' => $message,
),
200
);
}
/**
* Import remote inbox notifications.
*
* @param WP_REST_Request $request The full request data.
*
* @return WP_REST_Response
*/
public static function import( $request ) {
// Get the JSON data from the request body.
$specs = json_decode( wp_json_encode( $request->get_json_params() ) );
WC_Beta_Tester_Remote_Inbox_Notifications_Helper::import( $specs );
return new WP_REST_Response(
array(
'success' => true,
'message' => 'Remote inbox notifications imported.',
),
200
);
}
}

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Adds a new tool to the WCA Test Helper that helps import remote inbox notifications from staging or production for testing purposes

View File

@ -14,6 +14,7 @@ import { default as Experiments } from '../experiments';
import { default as Features } from '../features';
import { default as RestAPIFilters } from '../rest-api-filters';
import RemoteSpecValidator from '../remote-spec-validator';
import RemoteInboxNotifications from '../remote-inbox-notifications';
const tabs = applyFilters( 'woocommerce_admin_test_helper_tabs', [
{
@ -21,11 +22,6 @@ const tabs = applyFilters( 'woocommerce_admin_test_helper_tabs', [
title: 'Options',
content: <Options />,
},
{
name: 'admin-notes',
title: 'Admin notes',
content: <AdminNotes />,
},
{
name: 'tools',
title: 'Tools',
@ -47,9 +43,9 @@ const tabs = applyFilters( 'woocommerce_admin_test_helper_tabs', [
content: <RestAPIFilters />,
},
{
name: 'remote-spec-validator',
title: 'Remote Spec Rule Validator',
content: <RemoteSpecValidator />,
name: 'remote-inbox-notifications',
title: 'Remote Inbox Notifications',
content: <RemoteInboxNotifications />,
},
] );

View File

@ -183,4 +183,44 @@ form.rest-api-filter-new-form {
.btn-validate {
float: right;
}
}
#wc-admin-test-helper-remote-inbox-notifications {
pre {
margin: 0;
padding: 0;
}
.action-btns {
justify-content: end;
display: flex;
gap: 5px;
margin-top: 10px;
margin-bottom: 10px;
.btn-danger {
margin-right: auto;
}
}
.notification-actions {
display: flex;
gap: 5px;
justify-content: end;
}
.components-notice {
margin: 0px 0px 10px 0px;
}
.button.staging {
background-color: #de9816;
color: #fff;
border-color: #de9816;
}
.column-status {
display: flex;
svg {
width: 18px;
height: 18px;
margin-left: 2px;
}
}
}

View File

@ -0,0 +1,8 @@
const TYPES = {
SET_NOTIFICATIONS: 'SET_NOTIFICATIONS',
SET_IS_LOADING: 'SET_IS_LOADING',
DELETE_NOTIFICATION: 'DELETE_NOTIFICATION',
SET_NOTICE: 'SET_NOTICE',
};
export default TYPES;

View File

@ -0,0 +1,140 @@
/**
* 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';
/**
* Initialize the state
*
* @param {Array} notifications
*/
export function setNotifications( notifications ) {
return {
type: TYPES.SET_NOTIFICATIONS,
notifications,
};
}
export function setLoadingState( isLoading ) {
return {
type: TYPES.SET_IS_LOADING,
isLoading,
};
}
export function setNotice( notice ) {
return {
type: TYPES.SET_NOTICE,
notice,
};
}
export function* importNotifications( notifications ) {
try {
yield apiFetch( {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
path: `${ API_NAMESPACE }/remote-inbox-notifications/import`,
body: JSON.stringify( notifications ),
} );
yield controls.dispatch(
STORE_KEY,
'invalidateResolutionForStoreSelector',
'getNotifications'
);
} catch ( error ) {
setNotice( {
message: 'Failed to import notifications',
status: 'error',
} );
}
}
export function* deleteNotification( id ) {
try {
yield apiFetch( {
method: 'POST',
path: `${ API_NAMESPACE }/remote-inbox-notifications/${ id }/delete`,
} );
yield {
type: TYPES.DELETE_NOTIFICATION,
id,
};
setNotice( {
message: 'Notifications deleted successfully.',
status: 'success',
} );
} catch {
setNotice( {
message: 'Failed to delete notification',
status: 'error',
} );
}
}
export function* testNotification( name ) {
try {
const response = yield apiFetch( {
method: 'GET',
path: `${ API_NAMESPACE }/remote-inbox-notifications/${ name }/test`,
} );
if ( response.success ) {
yield controls.dispatch(
STORE_KEY,
'invalidateResolutionForStoreSelector',
'getNotifications'
);
}
yield setNotice( {
message:
typeof response.message === 'string'
? response.message
: 'The following rules have failed.\n\n' +
JSON.stringify( response.message, null, 2 ),
status: response.success ? 'success' : 'error',
} );
} catch ( e ) {
setNotice( {
message: 'Failed to test notification',
status: 'error',
} );
}
}
export function* deleteAllNotifications() {
try {
yield apiFetch( {
method: 'POST',
path: `${ API_NAMESPACE }/remote-inbox-notifications/delete-all`,
} );
yield controls.dispatch(
STORE_KEY,
'invalidateResolutionForStoreSelector',
'getNotifications'
);
setNotice( {
message: 'All notifications deleted successfully.',
status: 'success',
} );
} catch {
setNotice( {
message: 'Failed to delete all notifications',
status: 'error',
} );
}
}

View File

@ -0,0 +1,2 @@
export const STORE_KEY = 'wc-admin-helper/remote-inbox-notifications';
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,48 @@
/**
* Internal dependencies
*/
import TYPES from './action-types';
const DEFAULT_STATE = {
notifications: [],
isLoading: true,
notice: {
status: 'success',
message: '',
},
};
const reducer = ( state = DEFAULT_STATE, action ) => {
switch ( action.type ) {
case TYPES.SET_IS_LOADING:
return {
...state,
isLoading: action.isLoading,
};
case TYPES.SET_NOTIFICATIONS:
return {
...state,
notifications: action.notifications,
isLoading: false,
};
case TYPES.SET_NOTICE:
return {
...state,
notice: {
...state.notice,
...action.notice,
},
};
case TYPES.DELETE_NOTIFICATION:
return {
...state,
notifications: state.notifications.filter(
( item ) => item.note_id !== action.id
),
};
default:
return state;
}
};
export default reducer;

View File

@ -0,0 +1,23 @@
/**
* External dependencies
*/
import { apiFetch } from '@wordpress/data-controls';
/**
* Internal dependencies
*/
import { setLoadingState, setNotifications } from './actions';
export function* getNotifications() {
yield setLoadingState( true );
try {
const response = yield apiFetch( {
path: 'wc-admin-test-helper/remote-inbox-notifications',
} );
yield setNotifications( response );
} catch ( error ) {
throw new Error();
}
}

View File

@ -0,0 +1,11 @@
export function getNotifications( state ) {
return state.notifications;
}
export function isLoading( state ) {
return state.isLoading;
}
export function getNotice( state ) {
return state.notice;
}

View File

@ -0,0 +1,235 @@
/**
* External dependencies
*/
import { withDispatch, withSelect } from '@wordpress/data';
import { compose } from '@wordpress/compose';
import { Notice } from '@wordpress/components';
/**
* Internal dependencies
*/
import { STORE_KEY } from './data/constants';
import './data';
function RemoteInboxNotifications( {
notifications,
deleteNotification,
importNotifications,
deleteAllNotifications,
testNotification,
isLoading,
notice,
setNotice,
} ) {
const importFromUrl = async ( _url ) => {
const preDefinedUrls = {
staging:
'https://staging.woocommerce.com/wp-json/wccom/inbox-notifications/2.0/notifications.json',
production:
'https://woocommerce.com/wp-json/wccom/inbox-notifications/2.0/notifications.json',
};
const url = preDefinedUrls[ _url ] || _url;
try {
const response = await fetch( url );
const data = await response.json();
importNotifications( data );
setNotice( {
message: 'Notifications imported successfully.',
status: 'success',
} );
} catch ( error ) {
if ( _url === 'staging' ) {
const messages = {
staging:
'Failed to fetch notifications. Please make sure you are connected to Automattic proxy.',
production: error.message,
};
setNotice( {
message: messages[ _url ],
status: 'error',
} );
}
}
};
const renderLoading = () => {
return (
<tr>
<td colSpan="6" align="center">
Loading...
</td>
</tr>
);
};
const renderTableData = () => {
if ( notifications.length === 0 ) {
return (
<tr>
<td colSpan="5" align="center">
No Notifications Found
</td>
</tr>
);
}
return notifications.map( ( notification, index ) => {
return (
<tr key={ index }>
<td>{ notification.note_id }</td>
<td>{ notification.name }</td>
<td>{ notification.type }</td>
<td>{ notification.status }</td>
<td className="notification-actions">
<button
className="button btn"
onClick={ () => {
testNotification( notification.name );
} }
>
Run
</button>
<button
className="button btn-danger"
onClick={ () => {
if (
confirm(
'Are you sure you want to delete this notification?'
)
) {
deleteNotification( notification.note_id );
}
} }
>
Delete
</button>
</td>
</tr>
);
} );
};
return (
<>
<div id="wc-admin-test-helper-remote-inbox-notifications">
<div className="action-btns">
<input
type="button"
className="button btn-danger"
value="Delete All"
onClick={ () => {
if (
confirm(
'Are you sure you want to delete all notifications?'
)
) {
deleteAllNotifications();
}
} }
/>
<input
type="button"
className="button url"
value="Import from URL"
onClick={ () => {
const url = prompt(
'Enter the URL to import notifications from'
);
if ( url ) {
importFromUrl( url );
}
} }
/>
<input
type="button"
className="button btn-primary staging"
value="Import from staging"
onClick={ () => {
if (
confirm(
'Are you sure you want to import notifications from staging? Existing notifications will be overwritten.'
)
) {
importFromUrl( 'staging' );
}
} }
/>
<input
type="button"
className="button btn-primary"
value="Import from production"
onClick={ () => {
if (
confirm(
'Are you sure you want to import notifications from production? Existing notifications will be overwritten.'
)
) {
importFromUrl( 'production' );
}
} }
/>
</div>
{ notice.message.length > 0 && (
<Notice
status={ notice.status }
onRemove={ () => {
setNotice( { message: '' } );
} }
>
<pre>{ notice.message }</pre>
</Notice>
) }
<table className="wp-list-table striped table-view-list widefat">
<thead>
<tr>
<td className="manage-column column-thumb">I.D</td>
<td className="manage-column column-thumb">Name</td>
<td className="manage-column column-thumb align-center">
Type
</td>
<td className="manage-column column-thumb align-center">
Status
</td>
<td className="manage-column column-thumb align-center"></td>
</tr>
</thead>
<tbody>
{ isLoading ? renderLoading() : renderTableData() }
</tbody>
</table>
</div>
</>
);
}
export default compose(
withSelect( ( select ) => {
const { getNotifications, isLoading, getNotice } = select( STORE_KEY );
const notifications = getNotifications();
const notice = getNotice();
return {
notice,
notifications,
isLoading: isLoading(),
};
} ),
withDispatch( ( dispatch ) => {
const {
deleteNotification,
importNotifications,
deleteAllNotifications,
testNotification,
setNotice,
} = dispatch( STORE_KEY );
return {
testNotification,
deleteAllNotifications,
setNotice,
deleteNotification,
importNotifications,
};
} )
)( RemoteInboxNotifications );