merge master
This commit is contained in:
commit
7282e0bca6
|
@ -563,7 +563,7 @@ jQuery( function( $ ) {
|
|||
wc_checkout_form.$checkout_form.removeClass( 'processing' ).unblock();
|
||||
wc_checkout_form.$checkout_form.find( '.input-text, select, input:checkbox' ).trigger( 'validate' ).blur();
|
||||
wc_checkout_form.scroll_to_notices();
|
||||
$( document.body ).trigger( 'checkout_error' );
|
||||
$( document.body ).trigger( 'checkout_error' , [ error_message ] );
|
||||
},
|
||||
scroll_to_notices: function() {
|
||||
var scrollElement = $( '.woocommerce-NoticeGroup-updateOrderReview, .woocommerce-NoticeGroup-checkout' );
|
||||
|
|
|
@ -1,5 +1,64 @@
|
|||
== Changelog ==
|
||||
|
||||
= 4.7.0 - 2020-11-10 =
|
||||
|
||||
**WooCommerce**
|
||||
|
||||
* Tweak - Update `product_cat/tag` taxonomy template file names to `product-cat/tag`. #27736
|
||||
* Tweak - Exclude draft pages from the "Shop page" setting. #27890
|
||||
* Tweak - Styling to properly display product reviews within the dashboard activity widget. #27968
|
||||
* Fix - Fixes Photoswipe action buttons being obscured by admin bar. #27010
|
||||
* Fix - Allow variation image to be removed via REST API. #27299
|
||||
* Fix - Fixed WP CLI command to delete tax classes. #27310
|
||||
* Fix - Prevent regenerate image filter loop. #27483
|
||||
* Fix - Fixed some race conditions in `WC_Install`. #27696
|
||||
* Fix - Improved PHP 8 support for `Automattic\WooCommerce\RestApi\Utilities\SingletonTrait`. #27707
|
||||
* Fix - Adjust stock even if `reduce_stock` meta is not set in `wc_maybe_reduce_stock_levels`. #27763
|
||||
* Fix - Removed duplicated CSS code from jQuery UI. #27767
|
||||
* Fix - HTML syntax error in scheduled product message. #27842
|
||||
* Fix - Update logic to determine if an order requires payment to check the order instead of the cart. #27893
|
||||
* Fix - Use `Set password` title for lost password reset form when applicable. #27898
|
||||
* Fix - REST API - Fixed deprecated notices while querying orders and refunds through REST API v1 endpoints. #27934
|
||||
* Fix - Email address starting with `www` being displayed as a URL link in the admin order details page. #27983
|
||||
* Fix - Unexpected HTTP 401 "Sorry, you cannot list resources" REST API responses that occur when a plugin or custom code determines the current WordPress user before WooCommerce is fully initialized. #27587
|
||||
* Dev - Add `woocommerce_should_send_low_stock_notification` filter. #27819
|
||||
* Dev - Introduce (again) a dependency injection framework for the code in the src directory. #27733
|
||||
* Dev - Remove leftover code and data from the reverted improvement for variations filtering by attribute. #27748
|
||||
* Dev - Escaped labels in `woocommerce_form_field()`. #27800
|
||||
* Dev - Add a `NumberUtil::round` method to workaround a breaking change in the buil-in round function in PHP8. #27830
|
||||
* Dev - Remove default value from optional parameters that are followed by required parameters in functions/methods, since those are de-facto required and trigger a deprectation notice in PHP 8. #27840
|
||||
* Dev - REST API - Add user-friendly attribute names and values to order line items metadata.
|
||||
* Dev - REST API - Adds `parent_name` to `line_items` of the GET /orders endpoint.
|
||||
* Localization - Added Serbia districts. #27778
|
||||
* Localization - Make city, and postcode non-required fields. #27779
|
||||
* Localization - Add i18n locale information for Uganda, Kenya and Tanzania. #27164
|
||||
* Localization - Renamed "Postcode / ZIP" to "Pin code", and renamed "State / County" to "State" for India. #27516
|
||||
* Localization - Added postcode validation for addresses in India. #27546
|
||||
|
||||
**WooCommerce Blocks - 3.5.0 & 3.6.0**
|
||||
|
||||
* Make 'retry' property on errors from checkoutAfterProcessingWithSuccess/Error observers default to true if it's undefined. ([3261](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3261))
|
||||
* Ensure new payment methods are only displayed when no saved payment method is selected. ([3247](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3247))
|
||||
* Load WC Blocks CSS after editor CSS. ([3219](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3219))
|
||||
* Restore saved payment method data after closing an express payment method. ([3210](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3210))
|
||||
* Use light default background colour for country/state dropdowns. ([3189](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3189))
|
||||
* Fix broken Express Payment Method use in the Checkout block for logged out or incognito users. ([3165](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3165))
|
||||
* Fix State label for Spain. ([3147](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3147))
|
||||
* Don't throw an error when registering a payment method fails. ([3134](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3134))
|
||||
* Don't load contents of payment method hidden tabs. ([3227](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3227))
|
||||
* Use noticeContexts from useEmitResponse instead of hardcoded values. ([3161](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3161))
|
||||
|
||||
**WooCommerce Admin - 1.6.3**
|
||||
|
||||
* Tweak: Add BR and IN to list of stripe countries [#5377](https://github.com/woocommerce/woocommerce-admin/pull/5377)
|
||||
* Fix: Redirect instead of stalling on WCPay Inbox note action [#5413](https://github.com/woocommerce/woocommerce-admin/pull/5413)
|
||||
|
||||
= 4.6.2 - 2020-11-05 =
|
||||
|
||||
**WooCommerce**
|
||||
|
||||
* Prevent checkout from creating accounts when related setting is disabled.
|
||||
|
||||
= 4.6.1 - 2020-10-21 =
|
||||
|
||||
**WooCommerce**
|
||||
|
|
|
@ -259,6 +259,12 @@
|
|||
"provider",
|
||||
"service"
|
||||
],
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/philipobenito",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2020-09-28T13:38:44+00:00"
|
||||
},
|
||||
{
|
||||
|
@ -446,27 +452,22 @@
|
|||
},
|
||||
{
|
||||
"name": "symfony/css-selector",
|
||||
"version": "v3.4.45",
|
||||
"version": "v3.4.46",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/css-selector.git",
|
||||
"reference": "9ccf6e78077a3fc1596e6c7b5958008965a11518"
|
||||
"reference": "da3d9da2ce0026771f5fe64cb332158f1bd2bc33"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/css-selector/zipball/9ccf6e78077a3fc1596e6c7b5958008965a11518",
|
||||
"reference": "9ccf6e78077a3fc1596e6c7b5958008965a11518",
|
||||
"url": "https://api.github.com/repos/symfony/css-selector/zipball/da3d9da2ce0026771f5fe64cb332158f1bd2bc33",
|
||||
"reference": "da3d9da2ce0026771f5fe64cb332158f1bd2bc33",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^5.5.9|>=7.0.8"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "3.4-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\CssSelector\\": ""
|
||||
|
@ -495,7 +496,21 @@
|
|||
],
|
||||
"description": "Symfony CssSelector Component",
|
||||
"homepage": "https://symfony.com",
|
||||
"time": "2020-03-16T08:31:04+00:00"
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2020-10-24T10:57:07+00:00"
|
||||
},
|
||||
{
|
||||
"name": "woocommerce/action-scheduler",
|
||||
|
@ -686,5 +701,6 @@
|
|||
"platform-dev": [],
|
||||
"platform-overrides": {
|
||||
"php": "7.1"
|
||||
}
|
||||
},
|
||||
"plugin-api-version": "1.1.0"
|
||||
}
|
||||
|
|
|
@ -667,15 +667,18 @@ class WC_Checkout {
|
|||
* @return array of data.
|
||||
*/
|
||||
public function get_posted_data() {
|
||||
$skipped = array();
|
||||
// phpcs:disable WordPress.Security.NonceVerification.Missing
|
||||
$data = array(
|
||||
'terms' => (int) isset( $_POST['terms'] ), // WPCS: input var ok, CSRF ok.
|
||||
'createaccount' => (int) ! empty( $_POST['createaccount'] ), // WPCS: input var ok, CSRF ok.
|
||||
'payment_method' => isset( $_POST['payment_method'] ) ? wc_clean( wp_unslash( $_POST['payment_method'] ) ) : '', // WPCS: input var ok, CSRF ok.
|
||||
'shipping_method' => isset( $_POST['shipping_method'] ) ? wc_clean( wp_unslash( $_POST['shipping_method'] ) ) : '', // WPCS: input var ok, CSRF ok.
|
||||
'ship_to_different_address' => ! empty( $_POST['ship_to_different_address'] ) && ! wc_ship_to_billing_address_only(), // WPCS: input var ok, CSRF ok.
|
||||
'woocommerce_checkout_update_totals' => isset( $_POST['woocommerce_checkout_update_totals'] ), // WPCS: input var ok, CSRF ok.
|
||||
'terms' => (int) isset( $_POST['terms'] ),
|
||||
'createaccount' => (int) ( $this->is_registration_enabled() ? ! empty( $_POST['createaccount'] ) : false ),
|
||||
'payment_method' => isset( $_POST['payment_method'] ) ? wc_clean( wp_unslash( $_POST['payment_method'] ) ) : '',
|
||||
'shipping_method' => isset( $_POST['shipping_method'] ) ? wc_clean( wp_unslash( $_POST['shipping_method'] ) ) : '',
|
||||
'ship_to_different_address' => ! empty( $_POST['ship_to_different_address'] ) && ! wc_ship_to_billing_address_only(),
|
||||
'woocommerce_checkout_update_totals' => isset( $_POST['woocommerce_checkout_update_totals'] ),
|
||||
);
|
||||
// phpcs:enable WordPress.Security.NonceVerification.Missing
|
||||
|
||||
$skipped = array();
|
||||
foreach ( $this->get_checkout_fields() as $fieldset_key => $fieldset ) {
|
||||
if ( $this->maybe_skip_fieldset( $fieldset_key, $data ) ) {
|
||||
$skipped[] = $fieldset_key;
|
||||
|
|
|
@ -565,7 +565,7 @@ class WC_Shortcodes {
|
|||
$single_product = new WP_Query( $args );
|
||||
?>
|
||||
<script type="text/javascript">
|
||||
jQuery( document ).ready( function( $ ) {
|
||||
jQuery( function( $ ) {
|
||||
var $variations_form = $( '[data-product-page-preselected-id="<?php echo esc_attr( $preselected_id ); ?>"]' ).find( 'form.variations_form' );
|
||||
|
||||
<?php foreach ( $attributes as $attr => $value ) { ?>
|
||||
|
|
|
@ -56,6 +56,9 @@ class WC_Validation {
|
|||
case 'BA':
|
||||
$valid = (bool) preg_match( '/^([7-8]{1})([0-9]{4})$/', $postcode );
|
||||
break;
|
||||
case 'BE':
|
||||
$valid = (bool) preg_match( '/^([0-9]{4})$/i', $postcode );
|
||||
break;
|
||||
case 'BR':
|
||||
$valid = (bool) preg_match( '/^([0-9]{5})([-])?([0-9]{3})$/', $postcode );
|
||||
break;
|
||||
|
|
|
@ -102,7 +102,7 @@ class WC_Shop_Customizer {
|
|||
$max_notice = __( 'The maximum allowed setting is %d', 'woocommerce' );
|
||||
?>
|
||||
<script type="text/javascript">
|
||||
jQuery( document ).ready( function( $ ) {
|
||||
jQuery( function( $ ) {
|
||||
$( document.body ).on( 'change', '.woocommerce-cropping-control input[type="radio"]', function() {
|
||||
var $wrapper = $( this ).closest( '.woocommerce-cropping-control' ),
|
||||
value = $wrapper.find( 'input:checked' ).val();
|
||||
|
|
|
@ -108,8 +108,8 @@ abstract class Abstract_WC_Order_Data_Store_CPT extends WC_Data_Store_WP impleme
|
|||
$order->set_props(
|
||||
array(
|
||||
'parent_id' => $post_object->post_parent,
|
||||
'date_created' => 0 < $post_object->post_date_gmt ? wc_string_to_timestamp( $post_object->post_date_gmt ) : null,
|
||||
'date_modified' => 0 < $post_object->post_modified_gmt ? wc_string_to_timestamp( $post_object->post_modified_gmt ) : null,
|
||||
'date_created' => $this->string_to_timestamp( $post_object->post_date_gmt ),
|
||||
'date_modified' => $this->string_to_timestamp( $post_object->post_modified_gmt ),
|
||||
'status' => $post_object->post_status,
|
||||
)
|
||||
);
|
||||
|
|
|
@ -123,8 +123,8 @@ class WC_Coupon_Data_Store_CPT extends WC_Data_Store_WP implements WC_Coupon_Dat
|
|||
array(
|
||||
'code' => $post_object->post_title,
|
||||
'description' => $post_object->post_excerpt,
|
||||
'date_created' => 0 < $post_object->post_date_gmt ? wc_string_to_timestamp( $post_object->post_date_gmt ) : null,
|
||||
'date_modified' => 0 < $post_object->post_modified_gmt ? wc_string_to_timestamp( $post_object->post_modified_gmt ) : null,
|
||||
'date_created' => $this->string_to_timestamp( $post_object->post_date_gmt ),
|
||||
'date_modified' => $this->string_to_timestamp( $post_object->post_modified_gmt ),
|
||||
'date_expires' => metadata_exists( 'post', $coupon_id, 'date_expires' ) ? get_post_meta( $coupon_id, 'date_expires', true ) : get_post_meta( $coupon_id, 'expiry_date', true ), // @todo: Migrate expiry_date meta to date_expires in upgrade routine.
|
||||
'discount_type' => get_post_meta( $coupon_id, 'discount_type', true ),
|
||||
'amount' => get_post_meta( $coupon_id, 'coupon_amount', true ),
|
||||
|
|
|
@ -630,4 +630,16 @@ class WC_Data_Store_WP {
|
|||
);
|
||||
wp_cache_delete( 'lookup_table', 'object_' . $id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a WP post date string into a timestamp.
|
||||
*
|
||||
* @since 4.8.0
|
||||
*
|
||||
* @param string $time_string The WP post date string.
|
||||
* @return int|null The date string converted to a timestamp or null.
|
||||
*/
|
||||
protected function string_to_timestamp( $time_string ) {
|
||||
return '0000-00-00 00:00:00' !== $time_string ? wc_string_to_timestamp( $time_string ) : null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -170,8 +170,8 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
|
|||
array(
|
||||
'name' => $post_object->post_title,
|
||||
'slug' => $post_object->post_name,
|
||||
'date_created' => 0 < $post_object->post_date_gmt ? wc_string_to_timestamp( $post_object->post_date_gmt ) : null,
|
||||
'date_modified' => 0 < $post_object->post_modified_gmt ? wc_string_to_timestamp( $post_object->post_modified_gmt ) : null,
|
||||
'date_created' => $this->string_to_timestamp( $post_object->post_date_gmt ),
|
||||
'date_modified' => $this->string_to_timestamp( $post_object->post_modified_gmt ),
|
||||
'status' => $post_object->post_status,
|
||||
'description' => $post_object->post_content,
|
||||
'short_description' => $post_object->post_excerpt,
|
||||
|
|
|
@ -62,8 +62,8 @@ class WC_Product_Variation_Data_Store_CPT extends WC_Product_Data_Store_CPT impl
|
|||
array(
|
||||
'name' => $post_object->post_title,
|
||||
'slug' => $post_object->post_name,
|
||||
'date_created' => 0 < $post_object->post_date_gmt ? wc_string_to_timestamp( $post_object->post_date_gmt ) : null,
|
||||
'date_modified' => 0 < $post_object->post_modified_gmt ? wc_string_to_timestamp( $post_object->post_modified_gmt ) : null,
|
||||
'date_created' => $this->string_to_timestamp( $post_object->post_date_gmt ),
|
||||
'date_modified' => $this->string_to_timestamp( $post_object->post_modified_gmt ),
|
||||
'status' => $post_object->post_status,
|
||||
'menu_order' => $post_object->menu_order,
|
||||
'reviews_allowed' => 'open' === $post_object->comment_status,
|
||||
|
|
|
@ -316,15 +316,16 @@ function wc_cart_totals_order_total_html() {
|
|||
|
||||
if ( ! empty( $tax_string_array ) ) {
|
||||
$taxable_address = WC()->customer->get_taxable_address();
|
||||
/* translators: %s: country name */
|
||||
$estimated_text = WC()->customer->is_customer_outside_base() && ! WC()->customer->has_calculated_shipping() ? sprintf( ' ' . __( 'estimated for %s', 'woocommerce' ), WC()->countries->estimated_for_prefix( $taxable_address[0] ) . WC()->countries->countries[ $taxable_address[0] ] ) : '';
|
||||
$value .= '<small class="includes_tax">('
|
||||
/* translators: includes tax information */
|
||||
. esc_html__( 'includes', 'woocommerce' )
|
||||
. ' '
|
||||
. wp_kses_post( implode( ', ', $tax_string_array ) )
|
||||
. esc_html( $estimated_text )
|
||||
. ')</small>';
|
||||
if ( WC()->customer->is_customer_outside_base() && ! WC()->customer->has_calculated_shipping() ) {
|
||||
$country = WC()->countries->estimated_for_prefix( $taxable_address[0] ) . WC()->countries->countries[ $taxable_address[0] ];
|
||||
/* translators: 1: tax amount 2: country name */
|
||||
$tax_text = wp_kses_post( sprintf( __( '(includes %1$s estimated for %2$s)', 'woocommerce' ), implode( ', ', $tax_string_array ), $country ) );
|
||||
} else {
|
||||
/* translators: %s: tax amount */
|
||||
$tax_text = wp_kses_post( sprintf( __( '(includes %s)', 'woocommerce' ), implode( ', ', $tax_string_array ) ) );
|
||||
}
|
||||
|
||||
$value .= '<small class="includes_tax">' . $tax_text . '</small>';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -83,7 +83,6 @@
|
|||
"mocha": "7.2.0",
|
||||
"node-sass": "4.13.1",
|
||||
"prettier": "npm:wp-prettier@2.0.5",
|
||||
"puppeteer": "^2.1.1",
|
||||
"stylelint": "12.0.1",
|
||||
"stylelint-config-wordpress": "16.0.0",
|
||||
"typescript": "3.9.7",
|
||||
|
|
|
@ -4,7 +4,7 @@ Tags: e-commerce, store, sales, sell, woo, shop, cart, checkout, downloadable, d
|
|||
Requires at least: 5.3
|
||||
Tested up to: 5.5
|
||||
Requires PHP: 7.0
|
||||
Stable tag: 4.6.1
|
||||
Stable tag: 4.6.2
|
||||
License: GPLv3
|
||||
License URI: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
|
|
|
@ -24,24 +24,14 @@ The simplest way to use the client is directly:
|
|||
import { HTTPClientFactory } from '@woocommerce/api';
|
||||
|
||||
// You can create an API client using the client factory with pre-configured middleware for convenience.
|
||||
let httpClient = HTTPClientFactory.withBasicAuth(
|
||||
// The base URL of your REST API.
|
||||
'https://example.com/wp-json/',
|
||||
// The username for your WordPress user.
|
||||
'username',
|
||||
// The password for your WordPress user.
|
||||
'password',
|
||||
);
|
||||
let client = HTTPClientFactory.build( 'https://example.com' )
|
||||
.withBasicAuth( 'username', 'password' )
|
||||
.create();
|
||||
|
||||
// You can also create an API client configured for requests using OAuth.
|
||||
httpClient = HTTPClientFactory.withOAuth(
|
||||
// The base URL of your REST API.
|
||||
'https://example.com/wp-json/',
|
||||
// The OAuth API Key's consumer secret.
|
||||
'consumer_secret',
|
||||
// The OAuth API Key's consumer password.
|
||||
'consumer_pasword',
|
||||
);
|
||||
client = HTTPClientFactory.build( 'https://example.com' )
|
||||
.withOAuth( 'consumer_secret', 'consumer_password' )
|
||||
.create();
|
||||
|
||||
// You can then use the client to make API requests.
|
||||
httpClient.get( '/wc/v3/products' ).then( ( response ) => {
|
||||
|
@ -54,6 +44,7 @@ httpClient.get( '/wc/v3/products' ).then( ( response ) => {
|
|||
}, ( error ) => {
|
||||
// Handle errors that may have come up.
|
||||
} );
|
||||
|
||||
```
|
||||
|
||||
### Repositories
|
||||
|
@ -66,7 +57,9 @@ import { SimpleProduct } from '@woocommerce/api';
|
|||
|
||||
// Prepare the HTTP client that will be consumed by the repository.
|
||||
// This is necessary so that it can make requests to the REST API.
|
||||
const httpClient = HTTPClientFactory.withBasicAuth( 'https://example.com/wp-json/','username','password' );
|
||||
const httpClient = HTTPClientFactory.build( 'https://example.com' )
|
||||
.withBasicAuth( 'username', 'password' )
|
||||
.create();
|
||||
|
||||
const repository = SimpleProduct.restRepository( httpClient );
|
||||
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
import axios, { AxiosInstance } from 'axios';
|
||||
import * as moxios from 'moxios';
|
||||
import { AxiosURLToQueryInterceptor } from '../axios-url-to-query-interceptor';
|
||||
|
||||
describe( 'AxiosURLToQueryInterceptor', () => {
|
||||
let urlToQueryInterceptor: AxiosURLToQueryInterceptor;
|
||||
let axiosInstance: AxiosInstance;
|
||||
|
||||
beforeEach( () => {
|
||||
axiosInstance = axios.create();
|
||||
moxios.install( axiosInstance );
|
||||
urlToQueryInterceptor = new AxiosURLToQueryInterceptor( 'test' );
|
||||
urlToQueryInterceptor.start( axiosInstance );
|
||||
} );
|
||||
|
||||
afterEach( () => {
|
||||
urlToQueryInterceptor.stop( axiosInstance );
|
||||
moxios.uninstall();
|
||||
} );
|
||||
|
||||
it( 'should put path in query string', async () => {
|
||||
moxios.stubRequest( 'http://test.test/?test=%2Ftest%2Froute', {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
responseText: JSON.stringify( { test: 'value' } ),
|
||||
} );
|
||||
|
||||
const response = await axiosInstance.get( 'http://test.test/test/route' );
|
||||
|
||||
expect( response.status ).toEqual( 200 );
|
||||
} );
|
||||
} );
|
|
@ -1,4 +1,4 @@
|
|||
import { buildURL } from '../utils';
|
||||
import { buildURL, buildURLWithParams } from '../utils';
|
||||
|
||||
describe( 'buildURL', () => {
|
||||
it( 'should use base when given no url', () => {
|
||||
|
@ -15,9 +15,16 @@ describe( 'buildURL', () => {
|
|||
const url = buildURL( { baseURL: 'http://test.test', url: 'yes/test' } );
|
||||
expect( url ).toBe( 'http://test.test/yes/test' );
|
||||
} );
|
||||
} );
|
||||
|
||||
it( 'should combine base and url with trailing/leading slashes', () => {
|
||||
const url = buildURL( { baseURL: 'http://test.test/////', url: '////yes/test' } );
|
||||
expect( url ).toBe( 'http://test.test/yes/test' );
|
||||
describe( 'buildURLWithParams', () => {
|
||||
it( 'should do nothing without query string', () => {
|
||||
const url = buildURLWithParams( { baseURL: 'http://test.test' } );
|
||||
expect( url ).toBe( 'http://test.test' );
|
||||
} );
|
||||
|
||||
it( 'should append query string', () => {
|
||||
const url = buildURLWithParams( { baseURL: 'http://test.test', params: { test: 'yes' } } );
|
||||
expect( url ).toBe( 'http://test.test?test=yes' );
|
||||
} );
|
||||
} );
|
||||
|
|
|
@ -2,10 +2,10 @@ import type { AxiosRequestConfig } from 'axios';
|
|||
import * as createHmac from 'create-hmac';
|
||||
import * as OAuth from 'oauth-1.0a';
|
||||
import { AxiosInterceptor } from './axios-interceptor';
|
||||
import { buildURL } from './utils';
|
||||
import { buildURLWithParams } from './utils';
|
||||
|
||||
/**
|
||||
* A utility class for managing the lifecycle of an authentication interceptor.
|
||||
* An interceptor for adding OAuth 1.0a signatures to HTTP requests.
|
||||
*/
|
||||
export class AxiosOAuthInterceptor extends AxiosInterceptor {
|
||||
/**
|
||||
|
@ -14,7 +14,7 @@ export class AxiosOAuthInterceptor extends AxiosInterceptor {
|
|||
* @type {Object}
|
||||
* @private
|
||||
*/
|
||||
private oauth: OAuth;
|
||||
private readonly oauth: OAuth;
|
||||
|
||||
/**
|
||||
* Creates a new interceptor.
|
||||
|
@ -44,7 +44,7 @@ export class AxiosOAuthInterceptor extends AxiosInterceptor {
|
|||
* @return {AxiosRequestConfig} The request with the additional authorization headers.
|
||||
*/
|
||||
protected handleRequest( request: AxiosRequestConfig ): AxiosRequestConfig {
|
||||
const url = buildURL( request );
|
||||
const url = buildURLWithParams( request );
|
||||
if ( url.startsWith( 'https' ) ) {
|
||||
request.auth = {
|
||||
username: this.oauth.consumer.key,
|
||||
|
|
|
@ -2,6 +2,9 @@ import { AxiosResponse } from 'axios';
|
|||
import { AxiosInterceptor } from './axios-interceptor';
|
||||
import { HTTPResponse } from '../http-client';
|
||||
|
||||
/**
|
||||
* An interceptor for transforming the responses from axios into a consistent format for package consumers.
|
||||
*/
|
||||
export class AxiosResponseInterceptor extends AxiosInterceptor {
|
||||
/**
|
||||
* Transforms the Axios response into our HTTP response.
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
import { AxiosInterceptor } from './axios-interceptor';
|
||||
import { AxiosRequestConfig } from 'axios';
|
||||
import { buildURL } from './utils';
|
||||
|
||||
/**
|
||||
* An interceptor for transforming the request's path into a query parameter.
|
||||
*/
|
||||
export class AxiosURLToQueryInterceptor extends AxiosInterceptor {
|
||||
/**
|
||||
* The query parameter we want to assign the path to.
|
||||
*
|
||||
* @type {string}
|
||||
* @private
|
||||
*/
|
||||
private readonly queryParam: string;
|
||||
|
||||
/**
|
||||
* Constructs a new interceptor.
|
||||
*
|
||||
* @param {string} queryParam The query parameter we want to assign the path to.
|
||||
*/
|
||||
public constructor( queryParam: string ) {
|
||||
super();
|
||||
|
||||
this.queryParam = queryParam;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the outgoing path into a query parameter.
|
||||
*
|
||||
* @param {AxiosRequestConfig} config The axios config.
|
||||
* @return {AxiosRequestConfig} The axios config.
|
||||
*/
|
||||
protected handleRequest( config: AxiosRequestConfig ): AxiosRequestConfig {
|
||||
const url = new URL( buildURL( config ) );
|
||||
|
||||
// Store the path in the query string.
|
||||
if ( config.params instanceof URLSearchParams ) {
|
||||
config.params.set( this.queryParam, url.pathname );
|
||||
} else if ( config.params ) {
|
||||
config.params[ this.queryParam ] = url.pathname;
|
||||
} else {
|
||||
config.params = { [ this.queryParam ]: url.pathname };
|
||||
}
|
||||
|
||||
// Store the URL without the path now that it's in the query string.
|
||||
url.pathname = '';
|
||||
config.url = url.toString();
|
||||
delete config.baseURL;
|
||||
|
||||
return config;
|
||||
}
|
||||
}
|
|
@ -1,5 +1,10 @@
|
|||
import { AxiosRequestConfig } from 'axios';
|
||||
|
||||
// @ts-ignore
|
||||
import buildFullPath = require( 'axios/lib/core/buildFullPath' );
|
||||
// @ts-ignore
|
||||
import appendParams = require( 'axios/lib/helpers/buildURL' );
|
||||
|
||||
/**
|
||||
* Given an Axios request config this function generates the URL that Axios will
|
||||
* use to make the request.
|
||||
|
@ -8,17 +13,16 @@ import { AxiosRequestConfig } from 'axios';
|
|||
* @return {string} The merged URL.
|
||||
*/
|
||||
export function buildURL( request: AxiosRequestConfig ): string {
|
||||
const base = request.baseURL || '';
|
||||
if ( ! request.url ) {
|
||||
return base;
|
||||
}
|
||||
|
||||
// Axios ignores the base when the URL is absolute.
|
||||
const url = request.url;
|
||||
if ( ! base || url.match( /^([a-z][a-z\d+\-.]*:)?\/\/[^\/]/i ) ) {
|
||||
return url;
|
||||
}
|
||||
|
||||
// Remove trailing slashes from the base and leading slashes from the URL so we can combine them consistently.
|
||||
return base.replace( /\/+$/, '' ) + '/' + url.replace( /^\/+/, '' );
|
||||
return buildFullPath( request.baseURL, request.url );
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an Axios request config this function generates the URL that Axios will
|
||||
* use to make the request with the query parameters included.
|
||||
*
|
||||
* @param {AxiosRequestConfig} request The Axios request we're building the URL for.
|
||||
* @return {string} The merged URL.
|
||||
*/
|
||||
export function buildURLWithParams( request: AxiosRequestConfig ): string {
|
||||
return appendParams( buildURL( request ), request.params, request.paramsSerializer );
|
||||
}
|
||||
|
|
|
@ -1,39 +1,141 @@
|
|||
import { HTTPClient } from './http-client';
|
||||
import { AxiosClient, AxiosOAuthInterceptor } from './axios';
|
||||
import { AxiosRequestConfig } from 'axios';
|
||||
import { AxiosInterceptor } from './axios/axios-interceptor';
|
||||
import { AxiosURLToQueryInterceptor } from './axios/axios-url-to-query-interceptor';
|
||||
|
||||
/**
|
||||
* A class for generating HTTPClient instances with desired configurations.
|
||||
* These types describe the shape of the different auth methods our factory supports.
|
||||
*/
|
||||
type OAuthMethod = {
|
||||
type: 'oauth',
|
||||
key: string,
|
||||
secret: string,
|
||||
};
|
||||
type BasicAuthMethod = {
|
||||
type: 'basic',
|
||||
username: string,
|
||||
password: string,
|
||||
}
|
||||
|
||||
/**
|
||||
* An interface for describing the shape of a client to create using the factory.
|
||||
*/
|
||||
interface BuildParams {
|
||||
wpURL: string,
|
||||
useIndexPermalinks?: boolean,
|
||||
auth?: OAuthMethod | BasicAuthMethod,
|
||||
}
|
||||
|
||||
/**
|
||||
* A factory for generating an HTTPClient with a desired configuration.
|
||||
*/
|
||||
export class HTTPClientFactory {
|
||||
/**
|
||||
* Creates a new client instance prepared for basic auth.
|
||||
* The configuration object describing the client we're trying to create.
|
||||
*
|
||||
* @param {string} apiURL
|
||||
* @param {string} username
|
||||
* @param {string} password
|
||||
* @return {HTTPClient} An HTTP client configured for OAuth requests.
|
||||
* @private
|
||||
*/
|
||||
public static withBasicAuth( apiURL: string, username: string, password: string ): HTTPClient {
|
||||
return new AxiosClient(
|
||||
{
|
||||
baseURL: apiURL,
|
||||
auth: { username, password },
|
||||
},
|
||||
);
|
||||
private clientConfig: BuildParams;
|
||||
|
||||
private constructor( wpURL: string ) {
|
||||
this.clientConfig = { wpURL };
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new client instance prepared for oauth.
|
||||
* Creates a new factory that can be used to build clients.
|
||||
*
|
||||
* @param {string} apiURL
|
||||
* @param {string} consumerKey
|
||||
* @param {string} consumerSecret
|
||||
* @return {HTTPClient} An HTTP client configured for OAuth requests.
|
||||
* @param {string} wpURL The root URL of the WordPress installation we're querying.
|
||||
* @return {HTTPClientFactory} The new factory instance.
|
||||
*/
|
||||
public static withOAuth( apiURL: string, consumerKey: string, consumerSecret: string ): HTTPClient {
|
||||
return new AxiosClient(
|
||||
{ baseURL: apiURL },
|
||||
[ new AxiosOAuthInterceptor( consumerKey, consumerSecret ) ],
|
||||
public static build( wpURL: string ): HTTPClientFactory {
|
||||
return new HTTPClientFactory( wpURL );
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the client to utilize OAuth.
|
||||
*
|
||||
* @param {string} key The OAuth consumer key to use.
|
||||
* @param {string} secret The OAuth consumer secret to use.
|
||||
* @return {HTTPClientFactory} This factory.
|
||||
*/
|
||||
public withOAuth( key: string, secret: string ): this {
|
||||
this.clientConfig.auth = { type: 'oauth', key, secret };
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the client to utilize basic auth.
|
||||
*
|
||||
* @param {string} username The WordPress username to use.
|
||||
* @param {string} password The password for the WordPress user.
|
||||
* @return {HTTPClientFactory} This factory.
|
||||
*/
|
||||
public withBasicAuth( username: string, password: string ): this {
|
||||
this.clientConfig.auth = { type: 'basic', username, password };
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the client to use index permalinks.
|
||||
*
|
||||
* @return {HTTPClientFactory} This factory.
|
||||
*/
|
||||
public withIndexPermalinks(): this {
|
||||
this.clientConfig.useIndexPermalinks = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the client to use query permalinks.
|
||||
*
|
||||
* @return {HTTPClientFactory} This factory.
|
||||
*/
|
||||
public withoutIndexPermalinks(): this {
|
||||
this.clientConfig.useIndexPermalinks = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a client instance using the configuration stored within.
|
||||
*
|
||||
* @return {HTTPClient} The created client.
|
||||
*/
|
||||
public create(): HTTPClient {
|
||||
const axiosConfig: AxiosRequestConfig = {};
|
||||
const interceptors: AxiosInterceptor[] = [];
|
||||
|
||||
axiosConfig.baseURL = this.clientConfig.wpURL;
|
||||
if ( ! axiosConfig.baseURL.endsWith( '/' ) ) {
|
||||
axiosConfig.baseURL += '/';
|
||||
}
|
||||
|
||||
if ( this.clientConfig.useIndexPermalinks ) {
|
||||
axiosConfig.baseURL += 'wp-json/';
|
||||
} else {
|
||||
interceptors.push( new AxiosURLToQueryInterceptor( 'rest_route' ) );
|
||||
}
|
||||
|
||||
if ( this.clientConfig.auth ) {
|
||||
switch ( this.clientConfig.auth.type ) {
|
||||
case 'basic':
|
||||
axiosConfig.auth = {
|
||||
username: this.clientConfig.auth.username,
|
||||
password: this.clientConfig.auth.password,
|
||||
};
|
||||
break;
|
||||
|
||||
case 'oauth':
|
||||
interceptors.push(
|
||||
new AxiosOAuthInterceptor(
|
||||
this.clientConfig.auth.key,
|
||||
this.clientConfig.auth.secret,
|
||||
),
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return new AxiosClient( axiosConfig, interceptors );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,100 @@
|
|||
# WooCommerce Core End to End Test Suite
|
||||
|
||||
This package contains the automated end-to-end tests for WooCommerce.
|
||||
|
||||
## Table of contents
|
||||
|
||||
- [Pre-requisites](#pre-requisites)
|
||||
- [Setting up core tests](#setting-up-core-tests)
|
||||
- [Test functions](#test-functions)
|
||||
- [Activation and setup](#activation-and-setup)
|
||||
- [Merchant](#merchant)
|
||||
- [Shopper](#shopper)
|
||||
- [Contributing a new test](#contributing-a-new-test)
|
||||
|
||||
## Pre-requisites
|
||||
|
||||
### Setting up the test environment
|
||||
|
||||
Follow [E2E setup instructions](https://github.com/woocommerce/woocommerce/blob/master/tests/e2e/README.md).
|
||||
|
||||
### Setting up core tests
|
||||
|
||||
- Create the folder `tests/e2e/specs` in your repository if it does not exist.
|
||||
- To add a core test to your test suite, create a new `.test.js` file within `tests/e2e/specs` . Example code to run all the shopper tests:
|
||||
```js
|
||||
|
||||
const { runShopperTests } = require( '@woocommerce/e2e-core-tests' );
|
||||
|
||||
runShopperTests();
|
||||
|
||||
```
|
||||
|
||||
## Test functions
|
||||
|
||||
The functions to access the core tests are:
|
||||
|
||||
### Activation and setup
|
||||
|
||||
- `runSetupOnboardingTests` - Run all setup and onboarding tests
|
||||
- `runActivationTest` - Merchant can activate WooCommerce
|
||||
- `runOnboardingFlowTest` - Merchant can complete onboarding flow
|
||||
- `runTaskListTest` - Merchant can complete onboarding task list
|
||||
- `runInitialStoreSettingsTest` - Merchant can complete initial settings
|
||||
|
||||
### Merchant
|
||||
|
||||
- `runMerchantTests` - Run all merchant tests
|
||||
- `runCreateCouponTest` - Merchant can create coupon
|
||||
- `runCreateOrderTest` - Merchant can create order
|
||||
- `runAddSimpleProductTest` - Merchant can create a simple product
|
||||
- `runAddVariableProductTest` - Merchant can create a variable product
|
||||
- `runUpdateGeneralSettingsTest` - Merchant can update general settings
|
||||
- `runProductSettingsTest` - Merchant can update product settings
|
||||
- `runTaxSettingsTest` - Merchant can update tax settings
|
||||
|
||||
### Shopper
|
||||
|
||||
- `runShopperTests` - Run all shopper tests
|
||||
- `runCartPageTest` - Shopper can view and update cart
|
||||
- `runCheckoutPageTest` - Shopper can complete checkout
|
||||
- `runMyAccountPageTest` - Shopper can access my account page
|
||||
- `runSingleProductPageTest` - Shopper can view single product page
|
||||
|
||||
## Contributing a new test
|
||||
|
||||
- In your branch create a new `example-test-name.test.js` under the `tests/e2e/core-tests/specs` folder.
|
||||
- Jest does not allow its global functions to be accessed outside the jest environment. To allow the test code to be published in a package import any jest global functions used in your test
|
||||
```js
|
||||
const {
|
||||
it,
|
||||
describe,
|
||||
beforeAll,
|
||||
} = require( '@jest/globals' );
|
||||
```
|
||||
- Wrap your test in a function and export it
|
||||
```js
|
||||
const runExampleTestName = () => {
|
||||
describe('Example test', () => {
|
||||
beforeAll(async () => {
|
||||
// ...
|
||||
});
|
||||
|
||||
it('do some example action', async () => {
|
||||
// ...
|
||||
});
|
||||
// ...
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = runExampleTestName;
|
||||
```
|
||||
- Add your test to `tests/e2e/core-tests/specs/index.js`
|
||||
```js
|
||||
const runExampleTestName = require( './grouping/example-test-name.test' );
|
||||
// ...
|
||||
module.exports = {
|
||||
// ...
|
||||
runExampleTestName,
|
||||
}
|
||||
```
|
|
@ -1,44 +1,6 @@
|
|||
/*
|
||||
* Internal dependencies
|
||||
*/
|
||||
const {
|
||||
runActivationTest,
|
||||
runOnboardingFlowTest,
|
||||
runTaskListTest,
|
||||
runInitialStoreSettingsTest,
|
||||
runSetupOnboardingTests,
|
||||
runCartPageTest,
|
||||
runCheckoutPageTest,
|
||||
runMyAccountPageTest,
|
||||
runSingleProductPageTest,
|
||||
runShopperTests,
|
||||
runCreateCouponTest,
|
||||
runCreateOrderTest,
|
||||
runAddSimpleProductTest,
|
||||
runAddVariableProductTest,
|
||||
runUpdateGeneralSettingsTest,
|
||||
runProductSettingsTest,
|
||||
runTaxSettingsTest,
|
||||
runMerchantTests,
|
||||
} = require( './specs' );
|
||||
const allSpecs = require( './specs' );
|
||||
|
||||
module.exports = {
|
||||
runActivationTest,
|
||||
runOnboardingFlowTest,
|
||||
runTaskListTest,
|
||||
runInitialStoreSettingsTest,
|
||||
runSetupOnboardingTests,
|
||||
runCartPageTest,
|
||||
runCheckoutPageTest,
|
||||
runMyAccountPageTest,
|
||||
runSingleProductPageTest,
|
||||
runShopperTests,
|
||||
runCreateCouponTest,
|
||||
runCreateOrderTest,
|
||||
runAddSimpleProductTest,
|
||||
runAddVariableProductTest,
|
||||
runUpdateGeneralSettingsTest,
|
||||
runProductSettingsTest,
|
||||
runTaxSettingsTest,
|
||||
runMerchantTests,
|
||||
};
|
||||
module.exports = allSpecs;
|
||||
|
|
|
@ -42,7 +42,7 @@ const runCreateCouponTest = () => {
|
|||
// Publish coupon, verify that it was published. Trash coupon, verify that it was trashed.
|
||||
await verifyPublishAndTrash(
|
||||
'#publish',
|
||||
'#message',
|
||||
'.notice',
|
||||
'Coupon updated.',
|
||||
'1 coupon moved to the Trash.'
|
||||
);
|
||||
|
|
|
@ -196,9 +196,8 @@ const runAddVariableProductTest = () => {
|
|||
// Publish product, verify that it was published. Trash product, verify that it was trashed.
|
||||
await verifyPublishAndTrash(
|
||||
'#publish',
|
||||
'.updated.notice',
|
||||
'.notice',
|
||||
'Product published.',
|
||||
'Move to Trash',
|
||||
'1 product moved to the Trash.'
|
||||
);
|
||||
});
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -23,12 +23,11 @@
|
|||
"dependencies": {
|
||||
"@automattic/puppeteer-utils": "github:Automattic/puppeteer-utils#0f3ec50",
|
||||
"@jest/test-sequencer": "^25.5.4",
|
||||
"@wordpress/e2e-test-utils": "^4.6.0",
|
||||
"@wordpress/jest-preset-default": "^6.2.0",
|
||||
"@wordpress/e2e-test-utils": "^4.15.0",
|
||||
"@wordpress/jest-preset-default": "^6.4.0",
|
||||
"app-root-path": "^3.0.0",
|
||||
"jest": "^25.1.0",
|
||||
"jest-puppeteer": "^4.4.0",
|
||||
"puppeteer": "^2.1.1"
|
||||
"jest-puppeteer": "^4.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.12.0",
|
||||
|
@ -36,6 +35,11 @@
|
|||
"@babel/polyfill": "7.11.5",
|
||||
"@babel/preset-env": "7.12.0",
|
||||
"@wordpress/eslint-plugin": "7.1.0",
|
||||
"@babel/cli": "^7.12.1",
|
||||
"@babel/core": "^7.12.3",
|
||||
"@babel/polyfill": "^7.12.1",
|
||||
"@babel/preset-env": "^7.12.1",
|
||||
"@wordpress/eslint-plugin": "^4.0.0",
|
||||
"ndb": "^1.1.5",
|
||||
"semver": "^7.3.2"
|
||||
},
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
import { HTTPClientFactory } from '@woocommerce/api';
|
||||
const config = require( 'config' );
|
||||
|
||||
const httpClient = HTTPClientFactory.withBasicAuth(
|
||||
config.get( 'url' ) + '/wp-json',
|
||||
config.get( 'users.admin.username' ),
|
||||
config.get( 'users.admin.password' ),
|
||||
);
|
||||
const httpClient = HTTPClientFactory.build( config.get( 'url' ) )
|
||||
.withBasicAuth( config.get( 'users.admin.username' ), config.get( 'users.admin.password' ) )
|
||||
.create();
|
||||
|
||||
import { simpleProductFactory } from './factories/simple-product';
|
||||
|
||||
|
|
|
@ -110,7 +110,8 @@ const verifyPublishAndTrash = async ( button, publishNotice, publishVerification
|
|||
}
|
||||
|
||||
// Trash
|
||||
await expect( page ).toClick( 'a', { text: "Move to Trash" } );
|
||||
await page.focus( 'a.submitdelete' );
|
||||
await expect( page ).toClick( 'a.submitdelete' );
|
||||
await page.waitForSelector( '#message' );
|
||||
|
||||
// Verify
|
||||
|
|
Loading…
Reference in New Issue