Files
Geoff TaylorandGitHub 858887fa78 devops: Variable product performance optimization (#1006)
* devops: Variable product performance optimization and tests

- Memoize get_variation_prices() in the Product model to avoid
  redundant lookups across multiple pricing fields (price, regularPrice,
  salePrice, and their RAW variants)
- Remove redundant post__in filter from variations connection resolver;
  post_parent already constrains the query, and the post__in triggered
  an extra get_children() call per product
- Add createVariableProductCatalog() helper to GraphQLE2E for creating
  variable products with many variations in tests
- Add VariableProductPerformanceTest (wpunit) measuring DB query count
  and duration for 15 variable products with 18 variations each
- Add VariableProductPerformanceCest (functional) verifying 6 rapid
  queries do not return 429 errors

Addresses #897

* fix: Skip timing assertion when xdebug is active and clean up debug code
2026-03-31 09:17:44 -04:00

1390 lines
46 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace Helper;
// here you can define custom actions
// all public methods declared in helper class will be available in $I
class GraphQLE2E extends \Codeception\Module {
/**
* Asserts existence of and returns an array of HTTP response headers
*
* @param string|array $headers Headers to be evaluated and returned.
*
* @return array
*/
public function wantHTTPResponseHeaders( $headers ) {
$rest = $this->getModule( 'REST' );
if ( $headers && ! is_array( $headers ) ) {
$rest->seeHttpHeaderOnce( $headers );
return $rest->grabHttpHeader( $headers );
}
$response_headers = [];
foreach ( $headers as $header ) {
$rest->seeHttpHeaderOnce( $header );
$response_headers[] = $rest->grabHttpHeader( $header );
}
}
/**
* Authenticates User.
*
* @param array $input
* @param string $session_header
* @return array
*/
public function login( $input, $request_headers = [] ) {
$mutation = '
mutation ( $input: LoginInput! ) {
login( input: $input ) {
clientMutationId
authToken
refreshToken
customer {
databaseId
username
}
sessionToken
}
}
';
// Send GraphQL request and get response.
return $this->sendGraphQLRequest( $mutation, [ 'input' => $input ], $request_headers );
}
/**
* Adds item to cart.
*
* @param array $input
* @param string $session_header
*
* @return array
*/
public function addToCart( $input, $request_headers = [] ) {
// Add to cart mutation
$mutation = '
mutation ( $input: AddToCartInput! ) {
addToCart( input: $input ) {
clientMutationId
cartItem {
key
product {
node {
id
}
}
variation {
node {
id
}
}
quantity
subtotal
subtotalTax
total
tax
}
}
}
';
// Send GraphQL request and get response.
return $this->sendGraphQLRequest( $mutation, [ 'input' => $input ], $request_headers );
}
/**
* Update cart items quantities.
*
* @param array $input
* @param string $session_header
*
* @return array
*/
public function updateItemQuantities( $input, $request_headers = [] ) {
// Update cart items mutation
$mutation = '
mutation updateItemQuantities( $input: UpdateItemQuantitiesInput! ) {
updateItemQuantities( input: $input ) {
clientMutationId
updated {
key
quantity
}
removed {
key
quantity
}
items {
key
quantity
}
}
}
';
// Send GraphQL request and get response.
return $this->sendGraphQLRequest( $mutation, [ 'input' => $input ], $request_headers );
}
/**
* Removes an items from the cart.
*
* @param array $input
* @param string $session_header
* @return array
*/
public function removeItemsFromCart( $input, $request_headers = [] ) {
// Remove item from cart mutation
$mutation = '
mutation ( $input: RemoveItemsFromCartInput! ) {
removeItemsFromCart( input: $input ) {
clientMutationId
cartItems {
key
product {
node {
id
}
}
variation {
node {
id
}
}
quantity
subtotal
subtotalTax
total
tax
}
}
}
';
// Send GraphQL request and get response.
return $this->sendGraphQLRequest( $mutation, [ 'input' => $input ], $request_headers );
}
/**
* Restores items removed from the cart.
*
* @param array $input
* @param string $session_header
* @return array
*/
public function restoreCartItems( $input, $request_headers = [] ) {
$mutation = '
mutation restoreCartItems( $input: RestoreCartItemsInput! ) {
restoreCartItems( input: $input ) {
clientMutationId
cartItems {
key
product {
node {
id
}
}
variation {
node {
id
}
}
quantity
subtotal
subtotalTax
total
tax
}
}
}
';
// Send GraphQL request and get response.
return $this->sendGraphQLRequest( $mutation, [ 'input' => $input ], $request_headers );
}
/**
* Removes all items from the cart.
*
* @param array $input
* @param string $session_header
* @return array
*/
public function emptyCart( $input, $request_headers = [] ) {
$mutation = '
mutation emptyCart( $input: EmptyCartInput! ) {
emptyCart( input: $input ) {
clientMutationId
deletedCart {
contents {
nodes {
key
product {
node {
id
}
}
variation {
node {
id
}
}
quantity
subtotal
subtotalTax
total
tax
}
}
}
}
}
';
// Send GraphQL request and get response.
return $this->sendGraphQLRequest( $mutation, [ 'input' => $input ], $request_headers );
}
/**
* Adds fee on cart.
*
* @param array $input
* @param string $session_header
* @return array
*/
public function addFee( $input, $request_headers = [] ) {
$mutation = '
mutation addFee( $input: AddFeeInput! ) {
addFee( input: $input ) {
clientMutationId
cartFee {
id
name
taxClass
taxable
amount
total
}
}
}
';
// Send GraphQL request and get response.
return $this->sendGraphQLRequest( $mutation, [ 'input' => $input ], $request_headers );
}
/**
* Applies coupon to the cart.
*
* @param array $input
* @param string $session_header
* @return array
*/
public function applyCoupon( $input, $request_headers = [] ) {
$mutation = '
mutation applyCoupon( $input: ApplyCouponInput! ) {
applyCoupon( input: $input ) {
clientMutationId
cart {
appliedCoupons {
code
}
contents {
nodes {
key
product {
node {
node { id }
}
}
quantity
subtotal
subtotalTax
total
tax
}
}
}
}
}
';
// Send GraphQL request and get response.
return $this->sendGraphQLRequest( $mutation, [ 'input' => $input ], $request_headers );
}
/**
* Removes coupons on the cart.
*
* @param array $input
* @param string $session_header
* @return array
*/
public function removeCoupons( $input, $request_headers = [] ) {
$mutation = '
mutation removeCoupons( $input: RemoveCouponsInput! ) {
removeCoupons( input: $input ) {
clientMutationId
cart {
appliedCoupons {
code
}
contents {
nodes {
key
product {
node { id }
}
quantity
subtotal
subtotalTax
total
tax
}
}
}
}
}
';
// Send GraphQL request and get response.
return $this->sendGraphQLRequest( $mutation, [ 'input' => $input ], $request_headers );
}
/**
* Updates customers chosen shipping method.
*
* @param array $input
* @param string $session_header
* @return array
*/
public function updateShippingMethod( $input, $request_headers = [] ) {
// updateShippingMethod mutation.
$mutation = '
mutation ($input: UpdateShippingMethodInput!){
updateShippingMethod(input: $input) {
cart {
availableShippingMethods {
packageDetails
supportsShippingCalculator
rates {
id
cost
label
}
}
chosenShippingMethods
shippingTotal
shippingTax
subtotal
subtotalTax
total
}
}
}
';
// Send GraphQL request and get response.
return $this->sendGraphQLRequest( $mutation, [ 'input' => $input ], $request_headers );
}
/**
* Place customer order.
*
* @param array $input
* @param string $session_header
*
* @return array
*/
public function checkout( $input, $request_headers = [] ) {
// Checkout mutation.
$mutation = '
mutation checkout( $input: CheckoutInput! ) {
checkout( input: $input ) {
clientMutationId
order {
id
databaseId
currency
orderVersion
date
modified
status
discountTotal
discountTax
shippingTotal
shippingTax
cartTax
total
totalTax
subtotal
orderNumber
orderKey
createdVia
pricesIncludeTax
parent {
id
}
customer {
id
}
customerIpAddress
customerUserAgent
customerNote
billing {
firstName
lastName
company
address1
address2
city
state
postcode
country
email
phone
}
shipping {
firstName
lastName
company
address1
address2
city
state
postcode
country
}
paymentMethod
paymentMethodTitle
transactionId
dateCompleted
datePaid
cartHash
shippingAddressMapUrl
hasBillingAddress
hasShippingAddress
isDownloadPermitted
needsShippingAddress
hasDownloadableItem
downloadableItems {
nodes {
url
accessExpires
downloadId
downloadsRemaining
name
product {
databaseId
}
download {
downloadId
}
}
}
needsPayment
needsProcessing
couponLines {
nodes {
databaseId
orderId
code
discount
discountTax
coupon {
id
}
}
}
feeLines {
nodes {
databaseId
orderId
amount
name
taxStatus
total
totalTax
taxClass
}
}
shippingLines {
nodes {
databaseId
orderId
methodTitle
total
totalTax
taxClass
}
}
taxLines {
nodes {
rateCode
label
taxTotal
shippingTaxTotal
isCompound
taxRate {
databaseId
}
}
}
lineItems {
nodes {
productId
variationId
quantity
taxClass
subtotal
subtotalTax
total
totalTax
taxStatus
product {
node {
id
}
}
variation {
node {
id
}
}
}
}
}
customer {
id
}
result
redirect
}
}
';
// Send GraphQL request and get response.
return $this->sendGraphQLRequest( $mutation, [ 'input' => $input ], $request_headers );
}
/**
* Sends GraphQL and returns a response
*
* @param string $mutation
* @param array $variables
* @param string|null $session_header
* @param bool $update_header
*
* @return array
*/
public function sendGraphQLRequest( $query, $variables, $request_headers = [] ) {
$rest = $this->getModule( 'REST' );
// Add item to cart.
$rest->haveHttpHeader( 'Content-Type', 'application/json' );
// Set request headers
foreach ( $request_headers as $header => $value ) {
$rest->haveHttpHeader( $header, $value );
}
// Send request.
$rest->sendPost(
'/graphql',
json_encode(
[
'query' => $query,
'variables' => $variables,
]
)
);
// Confirm success.
$rest->seeResponseCodeIs( 200 );
$rest->seeResponseIsJson();
// Get response.
$response = json_decode( $rest->grabResponse(), true );
// Remove extensions field (temporary fix).
unset( $response['extensions'] );
// use --debug flag to view
codecept_debug( json_encode( $response, JSON_PRETTY_PRINT ) );
// Delete request headers
foreach ( $request_headers as $header => $value ) {
$rest->deleteHeader( $header );
}
return $response;
}
/**
* Creates store products
*
* @return array
*/
public function getCatalog() {
$this->_setupStore();
// Clear stale data from previous tests so Apache doesn't
// pick up sessions, carts, or users from prior runs.
$db = $this->getModule( 'WPDb' );
$db->dontHavePostInDatabase( [ 'post_type' => 'product' ], true );
$product_catalog = [];
$products = [
[
'post_title' => 't-shirt',
'meta_input' => [
'_price' => 45,
'_regular_price' => 45,
],
],
[
'post_title' => 'jeans',
'meta_input' => [
'_price' => 60,
'_regular_price' => 60,
],
],
[
'post_title' => 'belt',
'meta_input' => [
'_price' => 45,
'_regular_price' => 45,
],
],
[
'post_title' => 'shoes',
'meta_input' => [
'_price' => 115,
'_regular_price' => 115,
],
],
[
'post_title' => 'socks',
'meta_input' => [
'_price' => 20,
'_regular_price' => 20,
],
],
];
foreach ( $products as $product ) {
$this->haveAProductInTheDatabase( $product, $product_id );
$product_catalog[ $product['post_title'] ] = $product_id;
}
return $product_catalog;
}
/**
* Initializes store options and actions
*
* @param \Helper\AcceptanceTester $I
* @return void
*/
public function _setupStore() {
$wpdb = $this->getModule( 'WPDb' );
$wpdb->useTheme( 'twentytwentyone' );
// Turn on tax calculations and store shipping countries. Important!
$wpdb->haveOptionInDatabase( 'woocommerce_ship_to_countries', 'all' );
$wpdb->haveOptionInDatabase( 'woocommerce_prices_include_tax', 'no' );
$wpdb->haveOptionInDatabase( 'woocommerce_calc_taxes', 'yes' );
$wpdb->haveOptionInDatabase( 'woocommerce_tax_round_at_subtotal', 'no' );
// Enable payment gateway.
$wpdb->haveOptionInDatabase(
'woocommerce_bacs_settings',
[
'enabled' => 'yes',
'title' => 'Direct bank transfer',
'description' => 'Make your payment directly into our bank account. Please use your Order ID as the payment reference. Your order will not be shipped until the funds have cleared in our account.',
'instructions' => 'Instructions that will be added to the thank you page and emails.',
'account' => '',
]
);
// forcing has_block_template to be false
add_filter( 'woocommerce_has_block_template', '__return_false', 10, 2 );
//Additional cart fees.
add_action(
'woocommerce_cart_calculate_fees',
static function () {
$percentage = 0.01;
$surcharge = ( WC()->cart->cart_contents_total + WC()->cart->shipping_total ) * $percentage;
WC()->cart->add_fee( 'Surcharge', $surcharge, true, '' );
}
);
// Create Shipping Zones.
$zone = new \WC_Shipping_Zone();
$zone->set_zone_name( 'Local' );
$zone->set_zone_order( 1 );
$zone->add_location( 'GB', 'country' );
$zone->add_location( 'CB*', 'postcode' );
$zone->save();
$zone->add_shipping_method( 'flat_rate' );
$zone->add_shipping_method( 'free_shipping' );
$zone = new \WC_Shipping_Zone();
$zone->set_zone_name( 'Europe' );
$zone->set_zone_order( 2 );
$zone->add_location( 'EU', 'continent' );
$zone->save();
$zone->add_shipping_method( 'flat_rate' );
$zone->add_shipping_method( 'free_shipping' );
$zone = new \WC_Shipping_Zone();
$zone->set_zone_name( 'California' );
$zone->set_zone_order( 3 );
$zone->add_location( 'US:CA', 'state' );
$zone->save();
$zone->add_shipping_method( 'flat_rate' );
$zone->add_shipping_method( 'free_shipping' );
$zone = new \WC_Shipping_Zone();
$zone->set_zone_name( 'US' );
$zone->set_zone_order( 4 );
$zone->add_location( 'US', 'country' );
$zone->save();
$zone->add_shipping_method( 'flat_rate' );
$zone->add_shipping_method( 'free_shipping' );
$zone = new \WC_Shipping_Zone();
$zone->set_zone_name( 'US' );
$zone->set_zone_order( 5 );
$zone->add_location( 'US:NY', 'state' );
$zone->save();
$zone->add_shipping_method( 'flat_rate' );
$zone->add_shipping_method( 'free_shipping' );
global $wp_rewrite;
//Write the rule
$wp_rewrite->set_permalink_structure('/%postname%/');
//Set the option
update_option( "rewrite_rules", FALSE );
//Flush the rules and tell it to write htaccess
$wp_rewrite->flush_rules( true );
$this->_createWoocommercePages();
if ( function_exists( 'WC' ) && WC()->cart ) {
WC()->cart->empty_cart( true );
}
wp_set_current_user( 0 );
\WC()->customer = null;
\WC()->session = null;
\WC()->cart = null;
\WC()->initialize_session();
\WC()->initialize_cart();
}
private function _createWooCommercePages() {
$cart_page_content = '<!-- wp:woocommerce/cart -->
<div class="wp-block-woocommerce-cart alignwide is-loading"><!-- wp:woocommerce/filled-cart-block -->
<div class="wp-block-woocommerce-filled-cart-block"><!-- wp:woocommerce/cart-items-block -->
<div class="wp-block-woocommerce-cart-items-block"><!-- wp:woocommerce/cart-line-items-block -->
<div class="wp-block-woocommerce-cart-line-items-block"></div>
<!-- /wp:woocommerce/cart-line-items-block -->
<!-- wp:woocommerce/cart-cross-sells-block -->
<div class="wp-block-woocommerce-cart-cross-sells-block"><!-- wp:heading {"fontSize":"large"} -->
<h2 class="wp-block-heading has-large-font-size">' . __( 'You may be interested in…', 'woocommerce' ) . '</h2>
<!-- /wp:heading -->
<!-- wp:woocommerce/cart-cross-sells-products-block -->
<div class="wp-block-woocommerce-cart-cross-sells-products-block"></div>
<!-- /wp:woocommerce/cart-cross-sells-products-block --></div>
<!-- /wp:woocommerce/cart-cross-sells-block --></div>
<!-- /wp:woocommerce/cart-items-block -->
<!-- wp:woocommerce/cart-totals-block -->
<div class="wp-block-woocommerce-cart-totals-block"><!-- wp:woocommerce/cart-order-summary-block -->
<div class="wp-block-woocommerce-cart-order-summary-block"><!-- wp:woocommerce/cart-order-summary-heading-block -->
<div class="wp-block-woocommerce-cart-order-summary-heading-block"></div>
<!-- /wp:woocommerce/cart-order-summary-heading-block -->
<!-- wp:woocommerce/cart-order-summary-coupon-form-block -->
<div class="wp-block-woocommerce-cart-order-summary-coupon-form-block"></div>
<!-- /wp:woocommerce/cart-order-summary-coupon-form-block -->
<!-- wp:woocommerce/cart-order-summary-subtotal-block -->
<div class="wp-block-woocommerce-cart-order-summary-subtotal-block"></div>
<!-- /wp:woocommerce/cart-order-summary-subtotal-block -->
<!-- wp:woocommerce/cart-order-summary-fee-block -->
<div class="wp-block-woocommerce-cart-order-summary-fee-block"></div>
<!-- /wp:woocommerce/cart-order-summary-fee-block -->
<!-- wp:woocommerce/cart-order-summary-discount-block -->
<div class="wp-block-woocommerce-cart-order-summary-discount-block"></div>
<!-- /wp:woocommerce/cart-order-summary-discount-block -->
<!-- wp:woocommerce/cart-order-summary-shipping-block -->
<div class="wp-block-woocommerce-cart-order-summary-shipping-block"></div>
<!-- /wp:woocommerce/cart-order-summary-shipping-block -->
<!-- wp:woocommerce/cart-order-summary-taxes-block -->
<div class="wp-block-woocommerce-cart-order-summary-taxes-block"></div>
<!-- /wp:woocommerce/cart-order-summary-taxes-block --></div>
<!-- /wp:woocommerce/cart-order-summary-block -->
<!-- wp:woocommerce/cart-express-payment-block -->
<div class="wp-block-woocommerce-cart-express-payment-block"></div>
<!-- /wp:woocommerce/cart-express-payment-block -->
<!-- wp:woocommerce/proceed-to-checkout-block -->
<div class="wp-block-woocommerce-proceed-to-checkout-block"></div>
<!-- /wp:woocommerce/proceed-to-checkout-block -->
<!-- wp:woocommerce/cart-accepted-payment-methods-block -->
<div class="wp-block-woocommerce-cart-accepted-payment-methods-block"></div>
<!-- /wp:woocommerce/cart-accepted-payment-methods-block --></div>
<!-- /wp:woocommerce/cart-totals-block --></div>
<!-- /wp:woocommerce/filled-cart-block -->
<!-- wp:woocommerce/empty-cart-block -->
<div class="wp-block-woocommerce-empty-cart-block"><!-- wp:heading {"textAlign":"center","className":"with-empty-cart-icon wc-block-cart__empty-cart__title"} -->
<h2 class="wp-block-heading has-text-align-center with-empty-cart-icon wc-block-cart__empty-cart__title">' . __( 'Your cart is currently empty!', 'woocommerce' ) . '</h2>
<!-- /wp:heading -->
<!-- wp:separator {"className":"is-style-dots"} -->
<hr class="wp-block-separator has-alpha-channel-opacity is-style-dots"/>
<!-- /wp:separator -->
<!-- wp:heading {"textAlign":"center"} -->
<h2 class="wp-block-heading has-text-align-center">' . __( 'New in store', 'woocommerce' ) . '</h2>
<!-- /wp:heading -->
<!-- wp:woocommerce/product-new {"columns":4,"rows":1} /--></div>
<!-- /wp:woocommerce/empty-cart-block --></div>
<!-- /wp:woocommerce/cart -->';
$checkout_page_content = '<!-- wp:woocommerce/checkout -->
<div class="wp-block-woocommerce-checkout alignwide wc-block-checkout is-loading"><!-- wp:woocommerce/checkout-fields-block -->
<div class="wp-block-woocommerce-checkout-fields-block"><!-- wp:woocommerce/checkout-express-payment-block -->
<div class="wp-block-woocommerce-checkout-express-payment-block"></div>
<!-- /wp:woocommerce/checkout-express-payment-block -->
<!-- wp:woocommerce/checkout-contact-information-block -->
<div class="wp-block-woocommerce-checkout-contact-information-block"></div>
<!-- /wp:woocommerce/checkout-contact-information-block -->
<!-- wp:woocommerce/checkout-shipping-method-block -->
<div class="wp-block-woocommerce-checkout-shipping-method-block"></div>
<!-- /wp:woocommerce/checkout-shipping-method-block -->
<!-- wp:woocommerce/checkout-pickup-options-block -->
<div class="wp-block-woocommerce-checkout-pickup-options-block"></div>
<!-- /wp:woocommerce/checkout-pickup-options-block -->
<!-- wp:woocommerce/checkout-shipping-address-block -->
<div class="wp-block-woocommerce-checkout-shipping-address-block"></div>
<!-- /wp:woocommerce/checkout-shipping-address-block -->
<!-- wp:woocommerce/checkout-billing-address-block -->
<div class="wp-block-woocommerce-checkout-billing-address-block"></div>
<!-- /wp:woocommerce/checkout-billing-address-block -->
<!-- wp:woocommerce/checkout-shipping-methods-block -->
<div class="wp-block-woocommerce-checkout-shipping-methods-block"></div>
<!-- /wp:woocommerce/checkout-shipping-methods-block -->
<!-- wp:woocommerce/checkout-payment-block -->
<div class="wp-block-woocommerce-checkout-payment-block"></div>
<!-- /wp:woocommerce/checkout-payment-block -->
<!-- wp:woocommerce/checkout-additional-information-block -->
<div class="wp-block-woocommerce-checkout-additional-information-block"></div>
<!-- /wp:woocommerce/checkout-additional-information-block -->
<!-- wp:woocommerce/checkout-order-note-block -->
<div class="wp-block-woocommerce-checkout-order-note-block"></div>
<!-- /wp:woocommerce/checkout-order-note-block -->
<!-- wp:woocommerce/checkout-terms-block -->
<div class="wp-block-woocommerce-checkout-terms-block"></div>
<!-- /wp:woocommerce/checkout-terms-block -->
<!-- wp:woocommerce/checkout-actions-block -->
<div class="wp-block-woocommerce-checkout-actions-block"></div>
<!-- /wp:woocommerce/checkout-actions-block --></div>
<!-- /wp:woocommerce/checkout-fields-block -->
<!-- wp:woocommerce/checkout-totals-block -->
<div class="wp-block-woocommerce-checkout-totals-block"><!-- wp:woocommerce/checkout-order-summary-block -->
<div class="wp-block-woocommerce-checkout-order-summary-block"><!-- wp:woocommerce/checkout-order-summary-cart-items-block -->
<div class="wp-block-woocommerce-checkout-order-summary-cart-items-block"></div>
<!-- /wp:woocommerce/checkout-order-summary-cart-items-block -->
<!-- wp:woocommerce/checkout-order-summary-coupon-form-block -->
<div class="wp-block-woocommerce-checkout-order-summary-coupon-form-block"></div>
<!-- /wp:woocommerce/checkout-order-summary-coupon-form-block -->
<!-- wp:woocommerce/checkout-order-summary-subtotal-block -->
<div class="wp-block-woocommerce-checkout-order-summary-subtotal-block"></div>
<!-- /wp:woocommerce/checkout-order-summary-subtotal-block -->
<!-- wp:woocommerce/checkout-order-summary-fee-block -->
<div class="wp-block-woocommerce-checkout-order-summary-fee-block"></div>
<!-- /wp:woocommerce/checkout-order-summary-fee-block -->
<!-- wp:woocommerce/checkout-order-summary-discount-block -->
<div class="wp-block-woocommerce-checkout-order-summary-discount-block"></div>
<!-- /wp:woocommerce/checkout-order-summary-discount-block -->
<!-- wp:woocommerce/checkout-order-summary-shipping-block -->
<div class="wp-block-woocommerce-checkout-order-summary-shipping-block"></div>
<!-- /wp:woocommerce/checkout-order-summary-shipping-block -->
<!-- wp:woocommerce/checkout-order-summary-taxes-block -->
<div class="wp-block-woocommerce-checkout-order-summary-taxes-block"></div>
<!-- /wp:woocommerce/checkout-order-summary-taxes-block --></div>
<!-- /wp:woocommerce/checkout-order-summary-block --></div>
<!-- /wp:woocommerce/checkout-totals-block --></div>
<!-- /wp:woocommerce/checkout -->';
// Create cart page.
$wpdb = $this->getModule( 'WPDb' );
$cart_page_id = $wpdb->havePostInDatabase(
[
'post_type' => 'page',
'post_title' => 'Cart',
'post_name' => 'cart',
'post_author' => 1,
'post_content' => $cart_page_content,
'post_status' => 'publish',
]
);
$wpdb->haveOptionInDatabase( 'woocommerce_cart_page_id', $cart_page_id );
$checkout_page_id = $wpdb->havePostInDatabase(
[
'post_type' => 'page',
'post_title' => 'Checkout',
'post_name' => 'checkout',
'post_author' => 1,
'post_content' => $checkout_page_content,
'post_status' => 'publish',
]
);
$wpdb->haveOptionInDatabase( 'woocommerce_checkout_page_id', $checkout_page_id );
}
/**
* Adds Product in database
*
* @param \Helper\AcceptanceTester $I
* @param array $args Product args.
* @param integer $product_id ID for product being created.
* @param string $term Product type. Defaults to 'simple'.
* @param integer $term_id Product type term ID.
* @return void
*/
public function haveAProductInTheDatabase( $args, &$product_id, $term = 'simple', &$term_id = 0 ) {
$wpdb = $this->getModule( 'WPDb' );
// Create Product
$product_id = $wpdb->havePostInDatabase(
array_replace_recursive(
[
'post_type' => 'product',
'post_title' => 't-shirt',
'meta_input' => [
'_visibility' => 'visible',
'_sku' => '',
'_price' => '100',
'_regular_price' => '100',
'_sale_price' => '',
'_sale_date_on_sale_from' => null,
'_sale_date_on_sale_to' => null,
'total_sales' => '0',
'_tax_status' => 'taxable',
'_tax_class' => '',
'_manage_stock' => false,
'_stock_quantity' => null,
'_stock_status' => 'instock',
'_backorders' => 'no',
'_low_stock_amount' => '',
'_sold_individually' => false,
'_weight' => '',
'_length' => '',
'_width' => '',
'_height' => '',
'_upsell_ids' => [],
'_cross_sell_ids' => [],
'_purchase_note' => '',
'_default_attributes' => [],
'_product_attributes' => [],
'_virtual' => false,
'_downloadable' => false,
'_download_limit' => -1,
'_download_expiry' => -1,
'_featured' => false,
'_wc_rating_counts' => [],
'_wc_average_rating' => 0,
'_wc_review_count' => 0,
],
],
$args
)
);
if ( ! $term_id ) {
$term_id = $wpdb->grabTermIdFromDatabase(
[
'name' => $term,
'slug' => $term,
]
);
}
$term_taxonomy_id = $wpdb->grabTermTaxonomyIdFromDatabase(
[
'term_id' => $term_id,
'taxonomy' => 'product_type',
]
);
$wpdb->haveTermRelationshipInDatabase( $product_id, $term_id );
}
public function setupStoreAndUsers() {
$this->_setupStore();
$wpdb = $this->getModule( 'WPDb' );
// Clear stale data from previous tests so Apache doesn't
// pick up sessions, carts, or users from prior runs.
$wpdb->dontHaveInDatabase( 'wp_woocommerce_sessions', [] );
$wpdb->dontHaveUserInDatabase( 'jimbo1234', true );
$wpdb->dontHavePostInDatabase( [ 'post_type' => 'shop_order' ], true );
$wpdb->dontHavePostInDatabase( [ 'post_type' => 'shop_order_refund' ], true );
$userId = $wpdb->haveUserInDatabase(
'jimbo1234',
'customer',
[
'user_pass' => 'password',
'user_email' => 'jimbo1234@example.com',
]
);
return $userId;
}
public function verifyRedirect( $startUrl, $endUrl, $redirectCode = 301 ) {
$phpBrowser = $this->getModule( 'WPBrowser' );
$guzzle = $phpBrowser->client;
// Disable the following of redirects
$guzzle->followRedirects( false );
$phpBrowser->_loadPage( 'GET', $startUrl );
$response = $guzzle->getInternalResponse();
$responseCode = $response->getStatusCode();
$locationHeader = $response->getHeader( 'Location' );
$this->assertEquals( $responseCode, $redirectCode );
$this->assertEquals( $endUrl, $locationHeader );
$guzzle->followRedirects( true );
}
/**
* Sets a WordPress option in the database
*
* @param string $option_name Option name.
* @param mixed $option_value Option value.
*
* @return void
*/
public function haveOptionInDatabase( $option_name, $option_value ) {
$wpdb = $this->getModule( 'WPDb' );
$wpdb->haveOptionInDatabase( $option_name, $option_value );
}
/**
* Updates a single field in the woographql_settings option.
*
* @param string $field_name The setting field name.
* @param mixed $value The value to set.
*
* @return void
*/
public function setWooGraphQLSetting( $field_name, $value ) {
$wpdb = $this->getModule( 'WPDb' );
$existing = $wpdb->grabOptionFromDatabase( 'woographql_settings' );
$defaults = [
'disable_ql_session_handler' => 'off',
'enable_ql_session_handler_on_ajax' => 'off',
'enable_ql_session_handler_on_rest' => 'off',
'set_session_token_type' => 'legacy',
'session_transfer_behavior' => 'keep_new_fallback_old',
'enable_transliteration' => 'off',
'enable_unsupported_product_type' => 'off',
'enable_authorizing_url_fields' => [],
'authorizing_url_endpoint' => 'transfer-session',
'cart_url_nonce_param' => '_wc_cart',
'checkout_url_nonce_param' => '_wc_checkout',
'account_url_nonce_param' => '_wc_account',
'add_payment_method_url_nonce_param' => '_wc_payment',
'enable_pre_auth_download_urls' => 'off',
'download_url_nonce_param' => '_wc_download',
];
$settings = is_array( $existing ) ? array_merge( $defaults, $existing ) : $defaults;
$settings[ $field_name ] = $value;
$wpdb->haveOptionInDatabase( 'woographql_settings', $settings );
}
/**
* Gets the WordPress salt value for Store API Cart-Token
*
* The Store API uses '@' . wp_salt() as the secret.
* wp_salt() returns AUTH_KEY . AUTH_SALT from wp-config.php.
*
* @return string
*/
public function getStoreApiSecret() {
return '@' . wp_salt();
}
/**
* Validates a Store API Cart-Token JWT using WooCommerce's own JsonWebToken class.
*
* Uses the same class and secret that signed the token, avoiding cross-library
* signature mismatches between WooCommerce's JWT implementation and firebase/php-jwt.
*
* @param string $token The Cart-Token JWT string.
*
* @return array The decoded token payload.
*/
/**
* Creates a cart page with WooCommerce shortcode in the database
*
* @param string $slug The page slug. Defaults to 'cart-shortcode'.
* @return int The post ID of the created cart page
*/
public function haveACartShortcodePageInDatabase( $slug = 'cart-shortcode' ) {
$wpdb = $this->getModule( 'WPDb' );
$cart_page_id = $wpdb->havePostInDatabase(
[
'post_type' => 'page',
'post_title' => 'Cart Shortcode',
'post_name' => $slug,
'post_author' => 1,
'post_status' => 'publish',
'post_content' => '[woocommerce_cart]',
]
);
return $cart_page_id;
}
/**
* Creates a checkout page with WooCommerce shortcode in the database
*
* @param string $slug The page slug. Defaults to 'checkout-shortcode'.
* @return int The post ID of the created checkout page
*/
public function haveACheckoutShortcodePageInDatabase( $slug = 'checkout-shortcode' ) {
$wpdb = $this->getModule( 'WPDb' );
$checkout_page_id = $wpdb->havePostInDatabase(
[
'post_type' => 'page',
'post_title' => 'Checkout Shortcode',
'post_name' => $slug,
'post_author' => 1,
'post_status' => 'publish',
'post_content' => '[woocommerce_checkout]',
]
);
return $checkout_page_id;
}
/**
* Creates a my account page with WooCommerce shortcode in the database
*
* @param string $slug The page slug. Defaults to 'my-account-shortcode'.
* @return int The post ID of the created account page
*/
public function haveAnAccountShortcodePageInDatabase( $slug = 'my-account-shortcode' ) {
$wpdb = $this->getModule( 'WPDb' );
$account_page_id = $wpdb->havePostInDatabase(
[
'post_type' => 'page',
'post_title' => 'My Account Shortcode',
'post_name' => $slug,
'post_author' => 1,
'post_status' => 'publish',
'post_content' => '[woocommerce_my_account]',
]
);
return $account_page_id;
}
/**
* Creates a cart page with WooCommerce Cart Block in the database
*
* @return int The post ID of the created cart page
*/
public function haveACartBlockPageInDatabase() {
$wpdb = $this->getModule( 'WPDb' );
$cart_page_id = $wpdb->havePostInDatabase([
'post_type' => 'page',
'post_title' => 'Cart',
'post_name' => 'cart',
'post_author' => 1,
'post_status' => 'publish',
'post_content' => '<!-- wp:woocommerce/cart {"align":"wide"} -->
<div class="wp-block-woocommerce-cart alignwide is-loading"><!-- wp:woocommerce/filled-cart-block -->
<div class="wp-block-woocommerce-filled-cart-block"><!-- wp:woocommerce/cart-items-block -->
<div class="wp-block-woocommerce-cart-items-block"><!-- wp:woocommerce/cart-line-items-block -->
<div class="wp-block-woocommerce-cart-line-items-block"></div>
<!-- /wp:woocommerce/cart-line-items-block -->
<!-- wp:woocommerce/cart-cross-sells-block -->
<div class="wp-block-woocommerce-cart-cross-sells-block"></div>
<!-- /wp:woocommerce/cart-cross-sells-block --></div>
<!-- /wp:woocommerce/cart-items-block -->
<!-- wp:woocommerce/cart-totals-block -->
<div class="wp-block-woocommerce-cart-totals-block"><!-- wp:woocommerce/cart-order-summary-block -->
<div class="wp-block-woocommerce-cart-order-summary-block"></div>
<!-- /wp:woocommerce/cart-order-summary-block -->
<!-- wp:woocommerce/cart-express-payment-block -->
<div class="wp-block-woocommerce-cart-express-payment-block"></div>
<!-- /wp:woocommerce/cart-express-payment-block -->
<!-- wp:woocommerce/proceed-to-checkout-block -->
<div class="wp-block-woocommerce-proceed-to-checkout-block"></div>
<!-- /wp:woocommerce/proceed-to-checkout-block -->
<!-- wp:woocommerce/cart-accepted-payment-methods-block -->
<div class="wp-block-woocommerce-cart-accepted-payment-methods-block"></div>
<!-- /wp:woocommerce/cart-accepted-payment-methods-block --></div>
<!-- /wp:woocommerce/cart-totals-block --></div>
<!-- /wp:woocommerce/filled-cart-block -->
<!-- wp:woocommerce/empty-cart-block -->
<div class="wp-block-woocommerce-empty-cart-block"><!-- wp:heading {"textAlign":"center","className":"with-empty-cart-icon wc-block-cart__empty-cart__title"} -->
<h2 class="wp-block-heading has-text-align-center with-empty-cart-icon wc-block-cart__empty-cart__title">Your cart is currently empty!</h2>
<!-- /wp:heading -->
<!-- wp:separator {"className":"is-style-dots"} -->
<hr class="wp-separator has-alpha-channel-opacity is-style-dots"/>
<!-- /wp:separator -->
<!-- wp:heading {"textAlign":"center"} -->
<h2 class="wp-block-heading has-text-align-center">Start shopping</h2>
<!-- /wp:heading -->
<!-- wp:woocommerce/all-products {"columns":3,"rows":1,"alignButtons":false,"contentVisibility":{"orderBy":true},"orderby":"date","layoutConfig":[["woocommerce/product-image",{"productId":0}],["woocommerce/product-title",{"productId":0}],["woocommerce/product-price",{"productId":0}],["woocommerce/product-rating",{"productId":0}],["woocommerce/product-button",{"productId":0}]]} -->
<div class="wp-block-woocommerce-all-products wc-block-all-products" data-attributes="{&quot;alignButtons&quot;:false,&quot;columns&quot;:3,&quot;contentVisibility&quot;:{&quot;orderBy&quot;:true},&quot;isPreview&quot;:false,&quot;layoutConfig&quot;:[[&quot;woocommerce/product-image&quot;,{&quot;productId&quot;:0}],[&quot;woocommerce/product-title&quot;,{&quot;productId&quot;:0}],[&quot;woocommerce/product-price&quot;,{&quot;productId&quot;:0}],[&quot;woocommerce/product-rating&quot;,{&quot;productId&quot;:0}],[&quot;woocommerce/product-button&quot;,{&quot;productId&quot;:0}]],&quot;orderby&quot;:&quot;date&quot;,&quot;rows&quot;:1,&quot;stockStatus&quot;:[&quot;instock&quot;,&quot;outofstock&quot;,&quot;onbackorder&quot;]}"></div>
<!-- /wp:woocommerce/all-products --></div>
<!-- /wp:woocommerce/empty-cart-block --></div>
<!-- /wp:woocommerce/cart -->',
'post_status' => 'publish',
'post_type' => 'page',
]);
// Set this as the WooCommerce cart page
$wpdb->haveOptionInDatabase( 'woocommerce_cart_page_id', $cart_page_id );
return $cart_page_id;
}
/**
* Creates variable products with many variations for performance testing.
*
* @param int $product_count Number of variable products to create.
* @param int $variations_per Approximate variations per product (3 attrs: sizes × colors × styles).
*
* @return array Product IDs.
*/
public function createVariableProductCatalog( $product_count = 10, $variations_per = 18 ) {
$product_ids = [];
for ( $p = 0; $p < $product_count; $p++ ) {
$product = new \WC_Product_Variable();
$product->set_name( "Variable Product {$p}" );
$product->set_status( 'publish' );
$size_attr = new \WC_Product_Attribute();
$size_attr->set_name( 'Size' );
$size_attr->set_options( [ 'Small', 'Medium', 'Large' ] );
$size_attr->set_visible( true );
$size_attr->set_variation( true );
$color_attr = new \WC_Product_Attribute();
$color_attr->set_name( 'Color' );
$color_attr->set_options( [ 'Red', 'Blue', 'Green' ] );
$color_attr->set_visible( true );
$color_attr->set_variation( true );
$style_attr = new \WC_Product_Attribute();
$style_attr->set_name( 'Style' );
$style_attr->set_options( [ 'Classic', 'Modern' ] );
$style_attr->set_visible( true );
$style_attr->set_variation( true );
$product->set_attributes( [ $size_attr, $color_attr, $style_attr ] );
$product_id = $product->save();
// 3 sizes × 3 colors × 2 styles = 18 variations per product.
$sizes = [ 'Small', 'Medium', 'Large' ];
$colors = [ 'Red', 'Blue', 'Green' ];
$styles = [ 'Classic', 'Modern' ];
$count = 0;
foreach ( $sizes as $size ) {
foreach ( $colors as $color ) {
foreach ( $styles as $style ) {
if ( $count >= $variations_per ) {
break 3;
}
$variation = new \WC_Product_Variation();
$variation->set_parent_id( $product_id );
$variation->set_attributes(
[
'size' => $size,
'color' => $color,
'style' => $style,
]
);
$variation->set_regular_price( 10 + $count );
if ( $count % 3 === 0 ) {
$variation->set_sale_price( 8 + $count );
}
$variation->set_stock_status( 'instock' );
$variation->save();
$count++;
}
}
}
wc_delete_product_transients( $product_id );
$product_ids[] = $product_id;
}
return $product_ids;
}
}