Merge branch 'trunk' into e2e-shopper-pay-order
This commit is contained in:
commit
f44c2fa549
|
@ -602,7 +602,12 @@ class WC_Countries {
|
|||
array(
|
||||
'{first_name}' => $args['first_name'],
|
||||
'{last_name}' => $args['last_name'],
|
||||
'{name}' => $args['first_name'] . ' ' . $args['last_name'],
|
||||
'{name}' => sprintf(
|
||||
/* translators: 1: first name 2: last name */
|
||||
_x( '%1$s %2$s', 'full name', 'woocommerce' ),
|
||||
$args['first_name'],
|
||||
$args['last_name']
|
||||
),
|
||||
'{company}' => $args['company'],
|
||||
'{address_1}' => $args['address_1'],
|
||||
'{address_2}' => $args['address_2'],
|
||||
|
@ -612,7 +617,14 @@ class WC_Countries {
|
|||
'{country}' => $full_country,
|
||||
'{first_name_upper}' => wc_strtoupper( $args['first_name'] ),
|
||||
'{last_name_upper}' => wc_strtoupper( $args['last_name'] ),
|
||||
'{name_upper}' => wc_strtoupper( $args['first_name'] . ' ' . $args['last_name'] ),
|
||||
'{name_upper}' => wc_strtoupper(
|
||||
sprintf(
|
||||
/* translators: 1: first name 2: last name */
|
||||
_x( '%1$s %2$s', 'full name', 'woocommerce' ),
|
||||
$args['first_name'],
|
||||
$args['last_name']
|
||||
)
|
||||
),
|
||||
'{company_upper}' => wc_strtoupper( $args['company'] ),
|
||||
'{address_1_upper}' => wc_strtoupper( $args['address_1'] ),
|
||||
'{address_2_upper}' => wc_strtoupper( $args['address_2'] ),
|
||||
|
|
|
@ -162,7 +162,7 @@ class WC_Shipping_Rate {
|
|||
}
|
||||
|
||||
/**
|
||||
* Set ID for the rate. This is usually a combination of the method and instance IDs.
|
||||
* Get ID for the rate. This is usually a combination of the method and instance IDs.
|
||||
*
|
||||
* @since 3.2.0
|
||||
* @return string
|
||||
|
@ -172,7 +172,7 @@ class WC_Shipping_Rate {
|
|||
}
|
||||
|
||||
/**
|
||||
* Set shipping method ID the rate belongs to.
|
||||
* Get shipping method ID the rate belongs to.
|
||||
*
|
||||
* @since 3.2.0
|
||||
* @return string
|
||||
|
@ -182,7 +182,7 @@ class WC_Shipping_Rate {
|
|||
}
|
||||
|
||||
/**
|
||||
* Set instance ID the rate belongs to.
|
||||
* Get instance ID the rate belongs to.
|
||||
*
|
||||
* @since 3.2.0
|
||||
* @return int
|
||||
|
@ -192,7 +192,7 @@ class WC_Shipping_Rate {
|
|||
}
|
||||
|
||||
/**
|
||||
* Set rate label.
|
||||
* Get rate label.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
|
@ -201,7 +201,7 @@ class WC_Shipping_Rate {
|
|||
}
|
||||
|
||||
/**
|
||||
* Set rate cost.
|
||||
* Get rate cost.
|
||||
*
|
||||
* @since 3.2.0
|
||||
* @return string
|
||||
|
@ -211,7 +211,7 @@ class WC_Shipping_Rate {
|
|||
}
|
||||
|
||||
/**
|
||||
* Set rate taxes.
|
||||
* Get rate taxes.
|
||||
*
|
||||
* @since 3.2.0
|
||||
* @return array
|
||||
|
|
|
@ -2298,12 +2298,14 @@ if ( ! function_exists( 'woocommerce_checkout_coupon_form' ) ) {
|
|||
* Output the Coupon form for the checkout.
|
||||
*/
|
||||
function woocommerce_checkout_coupon_form() {
|
||||
wc_get_template(
|
||||
'checkout/form-coupon.php',
|
||||
array(
|
||||
'checkout' => WC()->checkout(),
|
||||
)
|
||||
);
|
||||
if ( is_user_logged_in() || WC()->checkout()->is_registration_enabled() || ! WC()->checkout()->is_registration_required() ) {
|
||||
wc_get_template(
|
||||
'checkout/form-coupon.php',
|
||||
array(
|
||||
'checkout' => WC()->checkout(),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -39,8 +39,8 @@ if ( $total <= 1 ) {
|
|||
'add_args' => false,
|
||||
'current' => max( 1, $current ),
|
||||
'total' => $total,
|
||||
'prev_text' => '←',
|
||||
'next_text' => '→',
|
||||
'prev_text' => is_rtl() ? '→' : '←',
|
||||
'next_text' => is_rtl() ? '←' : '→',
|
||||
'type' => 'list',
|
||||
'end_size' => 3,
|
||||
'mid_size' => 3,
|
||||
|
|
|
@ -51,8 +51,8 @@ if ( ! comments_open() ) {
|
|||
apply_filters(
|
||||
'woocommerce_comment_pagination_args',
|
||||
array(
|
||||
'prev_text' => '←',
|
||||
'next_text' => '→',
|
||||
'prev_text' => is_rtl() ? '→' : '←',
|
||||
'next_text' => is_rtl() ? '←' : '→',
|
||||
'type' => 'list',
|
||||
)
|
||||
)
|
||||
|
|
|
@ -3,7 +3,8 @@
|
|||
## Added
|
||||
|
||||
- Support for the external product type.
|
||||
- Added support for grouped product type
|
||||
- Support for grouped product type.
|
||||
- Support for variable products and product variations.
|
||||
- Support for coupons.
|
||||
|
||||
# 0.1.1
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { Model } from '../../models';
|
||||
import {
|
||||
CreatesChildModels,
|
||||
CreatesModels,
|
||||
DeletesChildModels,
|
||||
DeletesModels,
|
||||
|
@ -90,7 +91,7 @@ describe( 'ModelRepository', () => {
|
|||
it( 'should create child', async () => {
|
||||
const model = new DummyChildModel();
|
||||
const callback = jest.fn().mockResolvedValue( model );
|
||||
const repository: CreatesModels< DummyChildParams > = new ModelRepository< DummyChildParams >(
|
||||
const repository: CreatesChildModels< DummyChildParams > = new ModelRepository< DummyChildParams >(
|
||||
null,
|
||||
callback,
|
||||
null,
|
||||
|
@ -98,7 +99,7 @@ describe( 'ModelRepository', () => {
|
|||
null,
|
||||
);
|
||||
|
||||
const created = await repository.create( { childName: 'test' } );
|
||||
const created = await repository.create( { parent: 'yes' }, { childName: 'test' } );
|
||||
expect( created ).toBe( model );
|
||||
expect( callback ).toHaveBeenCalledWith( { childName: 'test' } );
|
||||
} );
|
||||
|
|
|
@ -77,6 +77,20 @@ export type ListChildFn< T extends ModelRepositoryParams > = (
|
|||
*/
|
||||
export type CreateFn< T extends ModelRepositoryParams > = ( properties: Partial< ModelClass< T > > ) => Promise< ModelClass< T > >;
|
||||
|
||||
/**
|
||||
* A callback for creating a child model using a data source.
|
||||
*
|
||||
* @callback CreateChildFn
|
||||
* @param {ModelID} parent The parent identifier for the model.
|
||||
* @param {Partial.<T>} properties The properties of the model to create.
|
||||
* @return {Promise.<T>} Resolves to the created model.
|
||||
* @template {Model} T
|
||||
*/
|
||||
export type CreateChildFn< T extends ModelRepositoryParams > = (
|
||||
parent: ParentID< T >,
|
||||
properties: Partial< ModelClass< T > >
|
||||
) => Promise< ModelClass< T > >;
|
||||
|
||||
/**
|
||||
* A callback for reading a model using a data source.
|
||||
*
|
||||
|
@ -189,6 +203,21 @@ export interface CreatesModels< T extends ModelRepositoryParams > {
|
|||
create( properties: Partial< ModelClass< T > > ): Promise< ModelClass< T > >;
|
||||
}
|
||||
|
||||
/**
|
||||
* An interface for repositories that can create child models.
|
||||
*
|
||||
* @typedef CreatesChildModels
|
||||
* @property {CreateChildFn.<T>} create Creates a child model using the repository.
|
||||
* @template {Model} T
|
||||
* @template {ModelParentID} P
|
||||
*/
|
||||
export interface CreatesChildModels< T extends ModelRepositoryParams > {
|
||||
create(
|
||||
parent: HasParent< T, ParentID< T >, never >,
|
||||
properties: HasParent< T, Partial< ModelClass< T > >, never >,
|
||||
): Promise< ModelClass< T > >;
|
||||
}
|
||||
|
||||
/**
|
||||
* An interface for repositories that can read models.
|
||||
*
|
||||
|
@ -301,7 +330,7 @@ export class ModelRepository< T extends ModelRepositoryParams > implements
|
|||
* @type {CreateFn.<T>}
|
||||
* @private
|
||||
*/
|
||||
private readonly createHook: CreateFn< T > | null;
|
||||
private readonly createHook: HasParent< T, CreateChildFn< T >, CreateFn< T > > | null;
|
||||
|
||||
/**
|
||||
* The hook used to read models.
|
||||
|
@ -338,7 +367,7 @@ export class ModelRepository< T extends ModelRepositoryParams > implements
|
|||
*/
|
||||
public constructor(
|
||||
listHook: HasParent< T, ListChildFn< T >, ListFn< T > > | null,
|
||||
createHook: CreateFn< T > | null,
|
||||
createHook: HasParent< T, CreateChildFn< T >, CreateFn< T > > | null,
|
||||
readHook: HasParent< T, ReadChildFn< T >, ReadFn< T > > | null,
|
||||
updateHook: HasParent< T, UpdateChildFn< T >, UpdateFn< T > > | null,
|
||||
deleteHook: HasParent< T, DeleteChildFn< T >, DeleteFn > | null,
|
||||
|
@ -380,15 +409,28 @@ export class ModelRepository< T extends ModelRepositoryParams > implements
|
|||
/**
|
||||
* Creates a new model using the repository.
|
||||
*
|
||||
* @param {P|ModelID} propertiesOrParent The properties to create the model with or the model parent.
|
||||
* @param {Partial.<T>} properties The properties to create the model with.
|
||||
* @return {Promise.<T>} Resolves to the created model.
|
||||
*/
|
||||
public create( properties: Partial< ModelClass< T > > ): Promise< ModelClass< T > > {
|
||||
public create(
|
||||
propertiesOrParent?: HasParent< T, ParentID< T >, Partial< ModelClass<T> > >,
|
||||
properties?: HasParent< T, Partial< ModelClass<T> >, never >,
|
||||
): Promise< ModelClass< T > > {
|
||||
if ( ! this.createHook ) {
|
||||
throw new Error( 'The \'create\' operation is not supported on this model.' );
|
||||
}
|
||||
|
||||
return this.createHook( properties );
|
||||
if ( properties === undefined ) {
|
||||
return ( this.createHook as CreateFn< T > )(
|
||||
propertiesOrParent as Partial< ModelClass<T> >,
|
||||
);
|
||||
}
|
||||
|
||||
return ( this.createHook as CreateChildFn< T > )(
|
||||
propertiesOrParent as ParentID<T>,
|
||||
properties as Partial< ModelClass<T> >,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import { Model, ModelID } from '../../model';
|
||||
import { MetaData, PostStatus } from '../../shared-types';
|
||||
import { AbstractProductData } from './data';
|
||||
import { ModelID } from '../../model';
|
||||
import {
|
||||
CatalogVisibility,
|
||||
ProductAttribute,
|
||||
ProductImage,
|
||||
ProductTerm,
|
||||
ProductLinks,
|
||||
ProductAttribute,
|
||||
} from '../shared';
|
||||
|
||||
/**
|
||||
|
@ -31,7 +30,7 @@ export const buildProductURL = ( id: ModelID ) => baseProductURL() + id;
|
|||
/**
|
||||
* The base for all product types.
|
||||
*/
|
||||
export abstract class AbstractProduct extends Model {
|
||||
export abstract class AbstractProduct extends AbstractProductData {
|
||||
/**
|
||||
* The name of the product.
|
||||
*
|
||||
|
@ -46,34 +45,6 @@ export abstract class AbstractProduct extends Model {
|
|||
*/
|
||||
public readonly slug: string = '';
|
||||
|
||||
/**
|
||||
* The permalink of the product.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
public readonly permalink: string = '';
|
||||
|
||||
/**
|
||||
* The Id of the product.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
public readonly id: number = 0;
|
||||
|
||||
/**
|
||||
* The parent Id of the product.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
public readonly parentId: number = 0;
|
||||
|
||||
/**
|
||||
* The menu order assigned to the product.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
public readonly menuOrder: number = 0;
|
||||
|
||||
/**
|
||||
* The GMT datetime when the product was created.
|
||||
*
|
||||
|
@ -88,13 +59,6 @@ export abstract class AbstractProduct extends Model {
|
|||
*/
|
||||
public readonly modified: Date = new Date();
|
||||
|
||||
/**
|
||||
* The product's current post status.
|
||||
*
|
||||
* @type {PostStatus}
|
||||
*/
|
||||
public readonly postStatus: PostStatus = '';
|
||||
|
||||
/**
|
||||
* The product's short description.
|
||||
*
|
||||
|
@ -102,20 +66,6 @@ export abstract class AbstractProduct extends Model {
|
|||
*/
|
||||
public readonly shortDescription: string = '';
|
||||
|
||||
/**
|
||||
* The product's full description.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
public readonly description: string = '';
|
||||
|
||||
/**
|
||||
* The product's SKU.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
public readonly sku: string = '';
|
||||
|
||||
/**
|
||||
* An array of the categories this product is in.
|
||||
*
|
||||
|
@ -130,13 +80,6 @@ export abstract class AbstractProduct extends Model {
|
|||
*/
|
||||
public readonly tags: readonly ProductTerm[] = [];
|
||||
|
||||
/**
|
||||
* Indicates whether or not the product is currently able to be purchased.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
public readonly isPurchasable: boolean = true;
|
||||
|
||||
/**
|
||||
* Indicates whether or not the product should be featured.
|
||||
*
|
||||
|
@ -144,20 +87,6 @@ export abstract class AbstractProduct extends Model {
|
|||
*/
|
||||
public readonly isFeatured: boolean = false;
|
||||
|
||||
/**
|
||||
* The attributes for the product.
|
||||
*
|
||||
* @type {ReadonlyArray.<ProductAttribute>}
|
||||
*/
|
||||
public readonly attributes: readonly ProductAttribute[] = [];
|
||||
|
||||
/**
|
||||
* The images for the product.
|
||||
*
|
||||
* @type {ReadonlyArray.<ProductImage>}
|
||||
*/
|
||||
public readonly images: readonly ProductImage[] = [];
|
||||
|
||||
/**
|
||||
* Indicates whether or not the product should be visible in the catalog.
|
||||
*
|
||||
|
@ -165,55 +94,6 @@ export abstract class AbstractProduct extends Model {
|
|||
*/
|
||||
public readonly catalogVisibility: CatalogVisibility = CatalogVisibility.Everywhere;
|
||||
|
||||
/**
|
||||
* The current price of the product.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
public readonly price: string = '';
|
||||
|
||||
/**
|
||||
* The rendered HTML for the current price of the product.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
public readonly priceHtml: string = '';
|
||||
|
||||
/**
|
||||
* The regular price of the product when not discounted.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
public readonly regularPrice: string = '';
|
||||
|
||||
/**
|
||||
* Indicates whether or not the product is currently on sale.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
public readonly onSale: boolean = false;
|
||||
|
||||
/**
|
||||
* The price of the product when on sale.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
public readonly salePrice: string = '';
|
||||
|
||||
/**
|
||||
* The GMT datetime when the product should start to be on sale.
|
||||
*
|
||||
* @type {Date|null}
|
||||
*/
|
||||
public readonly saleStart: Date | null = null;
|
||||
|
||||
/**
|
||||
* The GMT datetime when the product should no longer be on sale.
|
||||
*
|
||||
* @type {Date|null}
|
||||
*/
|
||||
public readonly saleEnd: Date | null = null;
|
||||
|
||||
/**
|
||||
* The count of sales of the product
|
||||
*
|
||||
|
@ -250,11 +130,11 @@ export abstract class AbstractProduct extends Model {
|
|||
public readonly relatedIds: Array<number> = [];
|
||||
|
||||
/**
|
||||
* The extra metadata for the product.
|
||||
* The attributes for the product.
|
||||
*
|
||||
* @type {ReadonlyArray.<MetaData>}
|
||||
* @type {ReadonlyArray.<ProductAttribute>}
|
||||
*/
|
||||
public readonly metaData: readonly MetaData[] = [];
|
||||
public readonly attributes: readonly ProductAttribute[] = [];
|
||||
|
||||
/**
|
||||
* The products links.
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
import { Model } from '../../model';
|
||||
import { MetaData, PostStatus } from '../../shared-types';
|
||||
import { ProductImage, ProductLinks } from '../shared';
|
||||
|
||||
/**
|
||||
* Base product data.
|
||||
*/
|
||||
export abstract class AbstractProductData extends Model {
|
||||
/**
|
||||
* The permalink of the product.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
public readonly permalink: string = '';
|
||||
|
||||
/**
|
||||
* The Id of the product.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
public readonly id: number = 0;
|
||||
|
||||
/**
|
||||
* The parent Id of the product.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
public readonly parentId: number = 0;
|
||||
|
||||
/**
|
||||
* The menu order assigned to the product.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
public readonly menuOrder: number = 0;
|
||||
|
||||
/**
|
||||
* The GMT datetime when the product was created.
|
||||
*
|
||||
* @type {Date}
|
||||
*/
|
||||
public readonly created: Date = new Date();
|
||||
|
||||
/**
|
||||
* The GMT datetime when the product was last modified.
|
||||
*
|
||||
* @type {Date}
|
||||
*/
|
||||
public readonly modified: Date = new Date();
|
||||
|
||||
/**
|
||||
* The product's current post status.
|
||||
*
|
||||
* @type {PostStatus}
|
||||
*/
|
||||
public readonly postStatus: PostStatus = '';
|
||||
|
||||
/**
|
||||
* The product's full description.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
public readonly description: string = '';
|
||||
|
||||
/**
|
||||
* The product's SKU.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
public readonly sku: string = '';
|
||||
|
||||
/**
|
||||
* Indicates whether or not the product is currently able to be purchased.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
public readonly isPurchasable: boolean = true;
|
||||
|
||||
/**
|
||||
* The images for the product.
|
||||
*
|
||||
* @type {ReadonlyArray.<ProductImage>}
|
||||
*/
|
||||
public readonly images: readonly ProductImage[] = [];
|
||||
|
||||
/**
|
||||
* The extra metadata for the product.
|
||||
*
|
||||
* @type {ReadonlyArray.<MetaData>}
|
||||
*/
|
||||
public readonly metaData: readonly MetaData[] = [];
|
||||
|
||||
/**
|
||||
* The product data links.
|
||||
*
|
||||
* @type {ReadonlyArray.<ProductLinks>}
|
||||
*/
|
||||
public readonly links: ProductLinks = {
|
||||
collection: [ { href: '' } ],
|
||||
self: [ { href: '' } ],
|
||||
};
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
export * from './common';
|
||||
export * from './cross-sell';
|
||||
export * from './data';
|
||||
export * from './delivery';
|
||||
export * from './external';
|
||||
export * from './grouped';
|
||||
|
|
|
@ -40,7 +40,7 @@ export type GroupedProductRepositoryParams =
|
|||
* @typedef ListsGroupedProducts
|
||||
* @alias ListsModels.<GroupedProduct>
|
||||
*/
|
||||
export type ListsGroupedProducts = ListsModels< GroupedProductUpdateParams >;
|
||||
export type ListsGroupedProducts = ListsModels< GroupedProductRepositoryParams >;
|
||||
|
||||
/**
|
||||
* An interface for creating Grouped products using the repository.
|
||||
|
@ -48,7 +48,7 @@ export type ListsGroupedProducts = ListsModels< GroupedProductUpdateParams >;
|
|||
* @typedef CreatesGroupedProducts
|
||||
* @alias CreatesModels.<GroupedProduct>
|
||||
*/
|
||||
export type CreatesGroupedProducts = CreatesModels< GroupedProductUpdateParams >;
|
||||
export type CreatesGroupedProducts = CreatesModels< GroupedProductRepositoryParams >;
|
||||
|
||||
/**
|
||||
* An interface for reading Grouped products using the repository.
|
||||
|
@ -56,7 +56,7 @@ export type CreatesGroupedProducts = CreatesModels< GroupedProductUpdateParams >
|
|||
* @typedef ReadsGroupedProducts
|
||||
* @alias ReadsModels.<GroupedProduct>
|
||||
*/
|
||||
export type ReadsGroupedProducts = ReadsModels< GroupedProductUpdateParams >;
|
||||
export type ReadsGroupedProducts = ReadsModels< GroupedProductRepositoryParams >;
|
||||
|
||||
/**
|
||||
* An interface for updating Grouped products using the repository.
|
||||
|
@ -64,7 +64,7 @@ export type ReadsGroupedProducts = ReadsModels< GroupedProductUpdateParams >;
|
|||
* @typedef UpdatesGroupedProducts
|
||||
* @alias UpdatesModels.<GroupedProduct>
|
||||
*/
|
||||
export type UpdatesGroupedProducts = UpdatesModels< GroupedProductUpdateParams >;
|
||||
export type UpdatesGroupedProducts = UpdatesModels< GroupedProductRepositoryParams >;
|
||||
|
||||
/**
|
||||
* An interface for deleting Grouped products using the repository.
|
||||
|
@ -72,7 +72,7 @@ export type UpdatesGroupedProducts = UpdatesModels< GroupedProductUpdateParams >
|
|||
* @typedef DeletesGroupedProducts
|
||||
* @alias DeletesModels.<GroupedProduct>
|
||||
*/
|
||||
export type DeletesGroupedProducts = DeletesModels< GroupedProductUpdateParams >;
|
||||
export type DeletesGroupedProducts = DeletesModels< GroupedProductRepositoryParams >;
|
||||
|
||||
/**
|
||||
* The base for the Grouped product object.
|
||||
|
|
|
@ -3,3 +3,5 @@ export * from './shared';
|
|||
export * from './simple-product';
|
||||
export * from './grouped-product';
|
||||
export * from './external-product';
|
||||
export * from './variation';
|
||||
export * from './variable-product';
|
||||
|
|
|
@ -70,9 +70,9 @@ export class ProductDownload {
|
|||
}
|
||||
|
||||
/**
|
||||
* A product's attributes.
|
||||
* Attribute base class.
|
||||
*/
|
||||
export class ProductAttribute {
|
||||
export abstract class AbstractAttribute {
|
||||
/**
|
||||
* The ID of the attribute.
|
||||
*
|
||||
|
@ -86,7 +86,12 @@ export class ProductAttribute {
|
|||
* @type {string}
|
||||
*/
|
||||
public readonly name: string = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* A product's attributes.
|
||||
*/
|
||||
export class ProductAttribute extends AbstractAttribute {
|
||||
/**
|
||||
* The sort order of the attribute.
|
||||
*
|
||||
|
@ -121,6 +126,29 @@ export class ProductAttribute {
|
|||
* @param {Partial.<ProductAttribute>} properties The properties to set.
|
||||
*/
|
||||
public constructor( properties?: Partial< ProductAttribute > ) {
|
||||
super();
|
||||
Object.assign( this, properties );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default attributes for variable products.
|
||||
*/
|
||||
export class ProductDefaultAttribute extends AbstractAttribute {
|
||||
/**
|
||||
* The option selected for the attribute.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
public readonly option: string = '';
|
||||
|
||||
/**
|
||||
* Creates a new product default attribute.
|
||||
*
|
||||
* @param {Partial.<ProductDefaultAttribute>} properties The properties to set.
|
||||
*/
|
||||
public constructor( properties?: Partial< ProductDefaultAttribute > ) {
|
||||
super();
|
||||
Object.assign( this, properties );
|
||||
}
|
||||
}
|
||||
|
@ -220,6 +248,13 @@ export class ProductLinks {
|
|||
*/
|
||||
public readonly self: readonly ProductLinkItem[] = [];
|
||||
|
||||
/**
|
||||
* The link to the parent.
|
||||
*
|
||||
* @type {ReadonlyArray.<ProductLinkItem>}
|
||||
*/
|
||||
public readonly up?: readonly ProductLinkItem[] = [];
|
||||
|
||||
/**
|
||||
* Creates a new product link list.
|
||||
*
|
||||
|
|
|
@ -6,14 +6,22 @@
|
|||
*/
|
||||
export type StockStatus = 'instock' | 'outofstock' | 'onbackorder' | string
|
||||
|
||||
/**
|
||||
* Base product properties.
|
||||
*/
|
||||
export type ProductDataUpdateParams = 'created' | 'postStatus'
|
||||
| 'id' | 'permalink' | 'price' | 'priceHtml'
|
||||
| 'description' | 'sku' | 'attributes' | 'images'
|
||||
| 'regularPrice' | 'salePrice' | 'saleStart' | 'saleEnd'
|
||||
| 'metaData' | 'menuOrder' | 'parentId' | 'links';
|
||||
|
||||
/**
|
||||
* Properties common to all product types.
|
||||
*/
|
||||
export type ProductCommonUpdateParams = 'name' | 'slug' | 'created' | 'postStatus' | 'shortDescription'
|
||||
| 'id' | 'permalink' | 'type' | 'description' | 'sku' | 'categories' | 'tags' | 'isFeatured'
|
||||
| 'attributes' | 'images' | 'catalogVisibility' | 'allowReviews'
|
||||
| 'metaData' | 'menuOrder' | 'parentId' | 'relatedIds' | 'upsellIds'
|
||||
| 'links' | 'relatedIds' | 'menuOrder' | 'parentId';
|
||||
export type ProductCommonUpdateParams = 'name' | 'slug' | 'shortDescription'
|
||||
| 'categories' | 'tags' | 'isFeatured' | 'averageRating' | 'numRatings'
|
||||
| 'catalogVisibility' | 'allowReviews' | 'upsellIds' | 'type'
|
||||
& ProductDataUpdateParams;
|
||||
|
||||
/**
|
||||
* Cross sells property.
|
||||
|
@ -67,4 +75,4 @@ export type ProductDeliveryUpdateParams = 'daysToDownload' | 'downloadLimit' | '
|
|||
/**
|
||||
* Properties exclusive to the Variable product type.
|
||||
*/
|
||||
export type ProductVariableTypeUpdateParams = 'defaultAttributes' | 'variations';
|
||||
export type ProductVariableUpdateParams = 'defaultAttributes' | 'variations';
|
||||
|
|
|
@ -0,0 +1,173 @@
|
|||
import {
|
||||
AbstractProduct,
|
||||
IProductCommon,
|
||||
IProductCrossSells,
|
||||
IProductInventory,
|
||||
IProductSalesTax,
|
||||
IProductShipping,
|
||||
IProductUpSells,
|
||||
ProductSearchParams,
|
||||
} from './abstract';
|
||||
import {
|
||||
ProductInventoryUpdateParams,
|
||||
ProductCommonUpdateParams,
|
||||
ProductDefaultAttribute,
|
||||
ProductSalesTaxUpdateParams,
|
||||
ProductCrossUpdateParams,
|
||||
ProductShippingUpdateParams,
|
||||
ProductUpSellUpdateParams,
|
||||
ProductVariableUpdateParams,
|
||||
StockStatus,
|
||||
BackorderStatus,
|
||||
Taxability,
|
||||
} from './shared';
|
||||
import { HTTPClient } from '../../http';
|
||||
import { variableProductRESTRepository } from '../../repositories';
|
||||
import {
|
||||
CreatesModels,
|
||||
DeletesModels,
|
||||
ListsModels,
|
||||
ModelRepositoryParams,
|
||||
ReadsModels,
|
||||
UpdatesModels,
|
||||
} from '../../framework';
|
||||
|
||||
/**
|
||||
* The parameters that variable products can update.
|
||||
*/
|
||||
type VariableProductUpdateParams = ProductVariableUpdateParams
|
||||
& ProductCommonUpdateParams
|
||||
& ProductCrossUpdateParams
|
||||
& ProductInventoryUpdateParams
|
||||
& ProductSalesTaxUpdateParams
|
||||
& ProductShippingUpdateParams
|
||||
& ProductUpSellUpdateParams;
|
||||
/**
|
||||
* The parameters embedded in this generic can be used in the ModelRepository in order to give
|
||||
* type-safety in an incredibly granular way.
|
||||
*/
|
||||
export type VariableProductRepositoryParams =
|
||||
ModelRepositoryParams< VariableProduct, never, ProductSearchParams, VariableProductUpdateParams >;
|
||||
|
||||
/**
|
||||
* An interface for listing variable products using the repository.
|
||||
*
|
||||
* @typedef ListsVariableProducts
|
||||
* @alias ListsModels.<VariableProduct>
|
||||
*/
|
||||
export type ListsVariableProducts = ListsModels< VariableProductRepositoryParams >;
|
||||
|
||||
/**
|
||||
* An interface for creating variable products using the repository.
|
||||
*
|
||||
* @typedef CreatesVariableProducts
|
||||
* @alias CreatesModels.<VariableProduct>
|
||||
*/
|
||||
export type CreatesVariableProducts = CreatesModels< VariableProductRepositoryParams >;
|
||||
|
||||
/**
|
||||
* An interface for reading variable products using the repository.
|
||||
*
|
||||
* @typedef ReadsVariableProducts
|
||||
* @alias ReadsModels.<VariableProduct>
|
||||
*/
|
||||
export type ReadsVariableProducts = ReadsModels< VariableProductRepositoryParams >;
|
||||
|
||||
/**
|
||||
* An interface for updating variable products using the repository.
|
||||
*
|
||||
* @typedef UpdatesVariableProducts
|
||||
* @alias UpdatesModels.<VariableProduct>
|
||||
*/
|
||||
export type UpdatesVariableProducts = UpdatesModels< VariableProductRepositoryParams >;
|
||||
|
||||
/**
|
||||
* An interface for deleting variable products using the repository.
|
||||
*
|
||||
* @typedef DeletesVariableProducts
|
||||
* @alias DeletesModels.<VariableProduct>
|
||||
*/
|
||||
export type DeletesVariableProducts = DeletesModels< VariableProductRepositoryParams >;
|
||||
|
||||
/**
|
||||
* The base for the Variable product object.
|
||||
*/
|
||||
export class VariableProduct extends AbstractProduct implements
|
||||
IProductCommon,
|
||||
IProductCrossSells,
|
||||
IProductInventory,
|
||||
IProductSalesTax,
|
||||
IProductShipping,
|
||||
IProductUpSells {
|
||||
/**
|
||||
* @see ./abstracts/cross-sells.ts
|
||||
*/
|
||||
public readonly crossSellIds: Array<number> = [];
|
||||
|
||||
/**
|
||||
* @see ./abstracts/upsell.ts
|
||||
*/
|
||||
public readonly upSellIds: Array<number> = [];
|
||||
|
||||
/**
|
||||
* @see ./abstracts/inventory.ts
|
||||
*/
|
||||
public readonly onePerOrder: boolean = false;
|
||||
public readonly trackInventory: boolean = false;
|
||||
public readonly remainingStock: number = -1;
|
||||
public readonly stockStatus: StockStatus = ''
|
||||
public readonly backorderStatus: BackorderStatus = BackorderStatus.Allowed;
|
||||
public readonly canBackorder: boolean = false;
|
||||
public readonly isOnBackorder: boolean = false;
|
||||
|
||||
/**
|
||||
* @see ./abstracts/sales-tax.ts
|
||||
*/
|
||||
public readonly taxStatus: Taxability = Taxability.ProductAndShipping;
|
||||
public readonly taxClass: string = '';
|
||||
|
||||
/**
|
||||
* @see ./abstracts/shipping.ts
|
||||
*/
|
||||
public readonly weight: string = '';
|
||||
public readonly length: string = '';
|
||||
public readonly width: string = '';
|
||||
public readonly height: string = '';
|
||||
public readonly requiresShipping: boolean = false;
|
||||
public readonly isShippingTaxable: boolean = false;
|
||||
public readonly shippingClass: string = '';
|
||||
public readonly shippingClassId: number = 0;
|
||||
|
||||
/**
|
||||
* Default product attributes.
|
||||
*
|
||||
* @type {ReadonlyArray.<ProductDefaultAttribute>}
|
||||
*/
|
||||
public readonly defaultAttributes: readonly ProductDefaultAttribute[] = [];
|
||||
|
||||
/**
|
||||
* Product variations.
|
||||
*
|
||||
* @type {ReadonlyArray.<number>}
|
||||
*/
|
||||
public readonly variations: Array<number> = [];
|
||||
|
||||
/**
|
||||
* Creates a new Variable product instance with the given properties
|
||||
*
|
||||
* @param {Object} properties The properties to set in the object.
|
||||
*/
|
||||
public constructor( properties?: Partial< VariableProduct > ) {
|
||||
super();
|
||||
Object.assign( this, properties );
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a model repository configured for communicating via the REST API.
|
||||
*
|
||||
* @param {HTTPClient} httpClient The client for communicating via HTTP.
|
||||
*/
|
||||
public static restRepository( httpClient: HTTPClient ): ReturnType< typeof variableProductRESTRepository > {
|
||||
return variableProductRESTRepository( httpClient );
|
||||
}
|
||||
}
|
|
@ -0,0 +1,187 @@
|
|||
import { ModelID } from '../model';
|
||||
import {
|
||||
AbstractProductData,
|
||||
IProductDelivery,
|
||||
IProductInventory,
|
||||
IProductPrice,
|
||||
IProductSalesTax,
|
||||
IProductShipping,
|
||||
ProductSearchParams,
|
||||
} from './abstract';
|
||||
import {
|
||||
ProductDataUpdateParams,
|
||||
ProductDeliveryUpdateParams,
|
||||
ProductInventoryUpdateParams,
|
||||
ProductPriceUpdateParams,
|
||||
ProductSalesTaxUpdateParams,
|
||||
ProductShippingUpdateParams,
|
||||
ProductLinks,
|
||||
Taxability,
|
||||
ProductDownload,
|
||||
StockStatus,
|
||||
BackorderStatus,
|
||||
ProductDefaultAttribute,
|
||||
} from './shared';
|
||||
import {
|
||||
CreatesChildModels,
|
||||
DeletesChildModels,
|
||||
ListsChildModels,
|
||||
ModelRepositoryParams,
|
||||
ReadsChildModels,
|
||||
UpdatesChildModels,
|
||||
} from '../../framework';
|
||||
import { HTTPClient } from '../../http';
|
||||
import { productVariationRESTRepository } from '../../repositories';
|
||||
|
||||
/**
|
||||
* The parameters that product variations can update.
|
||||
*/
|
||||
type ProductVariationUpdateParams = ProductDataUpdateParams
|
||||
& ProductDeliveryUpdateParams
|
||||
& ProductInventoryUpdateParams
|
||||
& ProductPriceUpdateParams
|
||||
& ProductSalesTaxUpdateParams
|
||||
& ProductShippingUpdateParams;
|
||||
/**
|
||||
* The parameters embedded in this generic can be used in the ModelRepository in order to give
|
||||
* type-safety in an incredibly granular way.
|
||||
*/
|
||||
export type ProductVariationRepositoryParams =
|
||||
ModelRepositoryParams< ProductVariation, ModelID, ProductSearchParams, ProductVariationUpdateParams >;
|
||||
|
||||
/**
|
||||
* An interface for listing variable products using the repository.
|
||||
*
|
||||
* @typedef ListsProductVariations
|
||||
* @alias ListsModels.<ProductVariation>
|
||||
*/
|
||||
export type ListsProductVariations = ListsChildModels< ProductVariationRepositoryParams >;
|
||||
|
||||
/**
|
||||
* An interface for creating variable products using the repository.
|
||||
*
|
||||
* @typedef CreatesProductVariations
|
||||
* @alias CreatesModels.<ProductVariation>
|
||||
*/
|
||||
export type CreatesProductVariations = CreatesChildModels< ProductVariationRepositoryParams >;
|
||||
|
||||
/**
|
||||
* An interface for reading variable products using the repository.
|
||||
*
|
||||
* @typedef ReadsProductVariations
|
||||
* @alias ReadsModels.<ProductVariation>
|
||||
*/
|
||||
export type ReadsProductVariations = ReadsChildModels< ProductVariationRepositoryParams >;
|
||||
|
||||
/**
|
||||
* An interface for updating variable products using the repository.
|
||||
*
|
||||
* @typedef UpdatesProductVariations
|
||||
* @alias UpdatesModels.<ProductVariation>
|
||||
*/
|
||||
export type UpdatesProductVariations = UpdatesChildModels< ProductVariationRepositoryParams >;
|
||||
|
||||
/**
|
||||
* An interface for deleting variable products using the repository.
|
||||
*
|
||||
* @typedef DeletesProductVariations
|
||||
* @alias DeletesModels.<ProductVariation>
|
||||
*/
|
||||
export type DeletesProductVariations = DeletesChildModels< ProductVariationRepositoryParams >;
|
||||
|
||||
/**
|
||||
* The base for the product variation object.
|
||||
*/
|
||||
export class ProductVariation extends AbstractProductData implements
|
||||
IProductDelivery,
|
||||
IProductInventory,
|
||||
IProductPrice,
|
||||
IProductSalesTax,
|
||||
IProductShipping {
|
||||
/**
|
||||
* @see ./abstracts/delivery.ts
|
||||
*/
|
||||
public readonly isVirtual: boolean = false;
|
||||
public readonly isDownloadable: boolean = false;
|
||||
public readonly downloads: readonly ProductDownload[] = [];
|
||||
public readonly downloadLimit: number = -1;
|
||||
public readonly daysToDownload: number = -1;
|
||||
public readonly purchaseNote: string = '';
|
||||
|
||||
/**
|
||||
* @see ./abstracts/inventory.ts
|
||||
*/
|
||||
public readonly onePerOrder: boolean = false;
|
||||
public readonly trackInventory: boolean = false;
|
||||
public readonly remainingStock: number = -1;
|
||||
public readonly stockStatus: StockStatus = ''
|
||||
public readonly backorderStatus: BackorderStatus = BackorderStatus.Allowed;
|
||||
public readonly canBackorder: boolean = false;
|
||||
public readonly isOnBackorder: boolean = false;
|
||||
|
||||
/**
|
||||
* @see ./abstracts/price.ts
|
||||
*/
|
||||
public readonly price: string = '';
|
||||
public readonly priceHtml: string = '';
|
||||
public readonly regularPrice: string = '';
|
||||
public readonly onSale: boolean = false;
|
||||
public readonly salePrice: string = '';
|
||||
public readonly saleStart: Date | null = null;
|
||||
public readonly saleEnd: Date | null = null;
|
||||
|
||||
/**
|
||||
* @see ./abstracts/sales-tax.ts
|
||||
*/
|
||||
public readonly taxStatus: Taxability = Taxability.ProductAndShipping;
|
||||
public readonly taxClass: string = '';
|
||||
|
||||
/**
|
||||
* @see ./abstracts/shipping.ts
|
||||
*/
|
||||
public readonly weight: string = '';
|
||||
public readonly length: string = '';
|
||||
public readonly width: string = '';
|
||||
public readonly height: string = '';
|
||||
public readonly requiresShipping: boolean = false;
|
||||
public readonly isShippingTaxable: boolean = false;
|
||||
public readonly shippingClass: string = '';
|
||||
public readonly shippingClassId: number = 0;
|
||||
|
||||
/**
|
||||
* The variation links.
|
||||
*
|
||||
* @type {ReadonlyArray.<ProductLinks>}
|
||||
*/
|
||||
public readonly links: ProductLinks = {
|
||||
collection: [ { href: '' } ],
|
||||
self: [ { href: '' } ],
|
||||
up: [ { href: '' } ],
|
||||
};
|
||||
|
||||
/**
|
||||
* The attributes for the variation.
|
||||
*
|
||||
* @type {ReadonlyArray.<ProductDefaultAttribute>}
|
||||
*/
|
||||
public readonly attributes: readonly ProductDefaultAttribute[] = [];
|
||||
|
||||
/**
|
||||
* Creates a new product variation instance with the given properties
|
||||
*
|
||||
* @param {Object} properties The properties to set in the object.
|
||||
*/
|
||||
public constructor( properties?: Partial< ProductVariation > ) {
|
||||
super();
|
||||
Object.assign( this, properties );
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a model repository configured for communicating via the REST API.
|
||||
*
|
||||
* @param {HTTPClient} httpClient The client for communicating via HTTP.
|
||||
*/
|
||||
public static restRepository( httpClient: HTTPClient ): ReturnType< typeof productVariationRESTRepository > {
|
||||
return productVariationRESTRepository( httpClient );
|
||||
}
|
||||
}
|
|
@ -2,10 +2,14 @@ import { createProductTransformer } from './shared';
|
|||
import { groupedProductRESTRepository } from './grouped-product';
|
||||
import { simpleProductRESTRepository } from './simple-product';
|
||||
import { externalProductRESTRepository } from './external-product';
|
||||
import { variableProductRESTRepository } from './variable-product';
|
||||
import { productVariationRESTRepository } from './variation';
|
||||
|
||||
export {
|
||||
createProductTransformer,
|
||||
externalProductRESTRepository,
|
||||
groupedProductRESTRepository,
|
||||
simpleProductRESTRepository,
|
||||
variableProductRESTRepository,
|
||||
productVariationRESTRepository,
|
||||
};
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
} from '../../../framework';
|
||||
import {
|
||||
AbstractProduct,
|
||||
AbstractProductData,
|
||||
IProductCrossSells,
|
||||
IProductDelivery,
|
||||
IProductExternal,
|
||||
|
@ -26,6 +27,7 @@ import {
|
|||
ProductDownload,
|
||||
ProductImage,
|
||||
ProductTerm,
|
||||
VariableProduct,
|
||||
} from '../../../models';
|
||||
import { createMetaDataTransformer } from '../shared';
|
||||
|
||||
|
@ -110,6 +112,55 @@ function createProductDownloadTransformer(): ModelTransformer< ProductDownload >
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a transformer for the base product property data.
|
||||
*
|
||||
* @param {Array.<ModelTransformation>} transformations Optional transformers to add to the transformer.
|
||||
* @return {ModelTransformer} The created transformer.
|
||||
*/
|
||||
export function createProductDataTransformer< T extends AbstractProductData >(
|
||||
transformations?: ModelTransformation[],
|
||||
): ModelTransformer< T > {
|
||||
if ( ! transformations ) {
|
||||
transformations = [];
|
||||
}
|
||||
|
||||
transformations.push(
|
||||
new IgnorePropertyTransformation(
|
||||
[
|
||||
'date_created',
|
||||
'date_modified',
|
||||
],
|
||||
),
|
||||
new ModelTransformerTransformation( 'images', ProductImage, createProductImageTransformer() ),
|
||||
new ModelTransformerTransformation( 'metaData', MetaData, createMetaDataTransformer() ),
|
||||
new PropertyTypeTransformation(
|
||||
{
|
||||
created: PropertyType.Date,
|
||||
modified: PropertyType.Date,
|
||||
isPurchasable: PropertyType.Boolean,
|
||||
parentId: PropertyType.Integer,
|
||||
menuOrder: PropertyType.Integer,
|
||||
permalink: PropertyType.String,
|
||||
},
|
||||
),
|
||||
new KeyChangeTransformation< AbstractProductData >(
|
||||
{
|
||||
created: 'date_created_gmt',
|
||||
modified: 'date_modified_gmt',
|
||||
postStatus: 'status',
|
||||
isPurchasable: 'purchasable',
|
||||
metaData: 'meta_data',
|
||||
parentId: 'parent_id',
|
||||
menuOrder: 'menu_order',
|
||||
links: '_links',
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
return new ModelTransformer( transformations );
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a transformer for the shared properties of all products.
|
||||
*
|
||||
|
@ -127,56 +178,34 @@ export function createProductTransformer< T extends AbstractProduct >(
|
|||
|
||||
transformations.push(
|
||||
new AddPropertyTransformation( {}, { type } ),
|
||||
new IgnorePropertyTransformation(
|
||||
[
|
||||
'date_created',
|
||||
'date_modified',
|
||||
],
|
||||
),
|
||||
new ModelTransformerTransformation( 'categories', ProductTerm, createProductTermTransformer() ),
|
||||
new ModelTransformerTransformation( 'tags', ProductTerm, createProductTermTransformer() ),
|
||||
new ModelTransformerTransformation( 'attributes', ProductAttribute, createProductAttributeTransformer() ),
|
||||
new ModelTransformerTransformation( 'images', ProductImage, createProductImageTransformer() ),
|
||||
new ModelTransformerTransformation( 'metaData', MetaData, createMetaDataTransformer() ),
|
||||
new PropertyTypeTransformation(
|
||||
{
|
||||
created: PropertyType.Date,
|
||||
modified: PropertyType.Date,
|
||||
isPurchasable: PropertyType.Boolean,
|
||||
isFeatured: PropertyType.Boolean,
|
||||
allowReviews: PropertyType.Boolean,
|
||||
averageRating: PropertyType.Integer,
|
||||
numRatings: PropertyType.Integer,
|
||||
totalSales: PropertyType.Integer,
|
||||
parentId: PropertyType.Integer,
|
||||
menuOrder: PropertyType.Integer,
|
||||
permalink: PropertyType.String,
|
||||
relatedIds: PropertyType.Integer,
|
||||
},
|
||||
),
|
||||
new KeyChangeTransformation< AbstractProduct >(
|
||||
{
|
||||
created: 'date_created_gmt',
|
||||
modified: 'date_modified_gmt',
|
||||
postStatus: 'status',
|
||||
shortDescription: 'short_description',
|
||||
isPurchasable: 'purchasable',
|
||||
isFeatured: 'featured',
|
||||
catalogVisibility: 'catalog_visibility',
|
||||
allowReviews: 'reviews_allowed',
|
||||
averageRating: 'average_rating',
|
||||
numRatings: 'rating_count',
|
||||
metaData: 'meta_data',
|
||||
totalSales: 'total_sales',
|
||||
parentId: 'parent_id',
|
||||
menuOrder: 'menu_order',
|
||||
relatedIds: 'related_ids',
|
||||
links: '_links',
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
return new ModelTransformer( transformations );
|
||||
return createProductDataTransformer< T >( transformations );
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -412,6 +441,29 @@ export function createProductShippingTransformation(): ModelTransformation[] {
|
|||
return transformations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Variable product specific properties transformations
|
||||
*/
|
||||
export function createProductVariableTransformation(): ModelTransformation[] {
|
||||
const transformations = [
|
||||
new PropertyTypeTransformation(
|
||||
{
|
||||
id: PropertyType.Integer,
|
||||
name: PropertyType.String,
|
||||
option: PropertyType.String,
|
||||
variations: PropertyType.Integer,
|
||||
},
|
||||
),
|
||||
new KeyChangeTransformation< VariableProduct >(
|
||||
{
|
||||
defaultAttributes: 'default_attributes',
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
return transformations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformer for the properties unique to the external product type.
|
||||
*/
|
||||
|
@ -433,4 +485,3 @@ export function createProductExternalTransformation(): ModelTransformation[] {
|
|||
|
||||
return transformations;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
import { HTTPClient } from '../../../http';
|
||||
import { ModelRepository } from '../../../framework';
|
||||
import {
|
||||
VariableProduct,
|
||||
CreatesVariableProducts,
|
||||
DeletesVariableProducts,
|
||||
ListsVariableProducts,
|
||||
ReadsVariableProducts,
|
||||
VariableProductRepositoryParams,
|
||||
UpdatesVariableProducts,
|
||||
baseProductURL,
|
||||
buildProductURL,
|
||||
} from '../../../models';
|
||||
import {
|
||||
createProductTransformer,
|
||||
createProductCrossSellsTransformation,
|
||||
createProductInventoryTransformation,
|
||||
createProductSalesTaxTransformation,
|
||||
createProductShippingTransformation,
|
||||
createProductUpSellsTransformation,
|
||||
createProductVariableTransformation,
|
||||
} from './shared';
|
||||
import {
|
||||
restCreate,
|
||||
restDelete,
|
||||
restList,
|
||||
restRead,
|
||||
restUpdate,
|
||||
} from '../shared';
|
||||
|
||||
/**
|
||||
* Creates a new ModelRepository instance for interacting with models via the REST API.
|
||||
*
|
||||
* @param {HTTPClient} httpClient The HTTP client for the REST requests to be made using.
|
||||
* @return {
|
||||
* ListsVariableProducts|
|
||||
* CreatesVariableProducts|
|
||||
* ReadsVariableProducts|
|
||||
* UpdatesVariableProducts|
|
||||
* DeletesVariableProducts
|
||||
* } The created repository.
|
||||
*/
|
||||
export function variableProductRESTRepository( httpClient: HTTPClient ): ListsVariableProducts
|
||||
& CreatesVariableProducts
|
||||
& ReadsVariableProducts
|
||||
& UpdatesVariableProducts
|
||||
& DeletesVariableProducts {
|
||||
const crossSells = createProductCrossSellsTransformation();
|
||||
const inventory = createProductInventoryTransformation();
|
||||
const salesTax = createProductSalesTaxTransformation();
|
||||
const shipping = createProductShippingTransformation();
|
||||
const upsells = createProductUpSellsTransformation();
|
||||
const variable = createProductVariableTransformation();
|
||||
const transformations = [
|
||||
...crossSells,
|
||||
...inventory,
|
||||
...salesTax,
|
||||
...shipping,
|
||||
...upsells,
|
||||
...variable,
|
||||
];
|
||||
|
||||
const transformer = createProductTransformer<VariableProduct>( 'variable', transformations );
|
||||
|
||||
return new ModelRepository(
|
||||
restList< VariableProductRepositoryParams >( baseProductURL, VariableProduct, httpClient, transformer ),
|
||||
restCreate< VariableProductRepositoryParams >( baseProductURL, VariableProduct, httpClient, transformer ),
|
||||
restRead< VariableProductRepositoryParams >( buildProductURL, VariableProduct, httpClient, transformer ),
|
||||
restUpdate< VariableProductRepositoryParams >( buildProductURL, VariableProduct, httpClient, transformer ),
|
||||
restDelete< VariableProductRepositoryParams >( buildProductURL, httpClient ),
|
||||
);
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
import { HTTPClient } from '../../../http';
|
||||
import { ModelRepository } from '../../../framework';
|
||||
import {
|
||||
ProductVariation,
|
||||
ModelID,
|
||||
CreatesProductVariations,
|
||||
DeletesProductVariations,
|
||||
ListsProductVariations,
|
||||
ReadsProductVariations,
|
||||
ProductVariationRepositoryParams,
|
||||
UpdatesProductVariations,
|
||||
buildProductURL,
|
||||
} from '../../../models';
|
||||
import {
|
||||
createProductDataTransformer,
|
||||
createProductDeliveryTransformation,
|
||||
createProductInventoryTransformation,
|
||||
createProductPriceTransformation,
|
||||
createProductSalesTaxTransformation,
|
||||
createProductShippingTransformation,
|
||||
} from './shared';
|
||||
import {
|
||||
restCreateChild,
|
||||
restDeleteChild,
|
||||
restListChild,
|
||||
restReadChild,
|
||||
restUpdateChild,
|
||||
} from '../shared';
|
||||
|
||||
/**
|
||||
* Creates a new ModelRepository instance for interacting with models via the REST API.
|
||||
*
|
||||
* @param {HTTPClient} httpClient The HTTP client for the REST requests to be made using.
|
||||
* @return {
|
||||
* ListsProductVariations|
|
||||
* CreatesProductVariations|
|
||||
* ReadsProductVariations|
|
||||
* UpdatesProductVariations|
|
||||
* DeletesProductVariations
|
||||
* } The created repository.
|
||||
*/
|
||||
export function productVariationRESTRepository( httpClient: HTTPClient ): ListsProductVariations
|
||||
& CreatesProductVariations
|
||||
& ReadsProductVariations
|
||||
& UpdatesProductVariations
|
||||
& DeletesProductVariations {
|
||||
const buildURL = ( parent: ModelID ) => buildProductURL( parent ) + '/variations/';
|
||||
const buildChildURL = ( parent: ModelID, id: ModelID ) => buildURL( parent ) + id;
|
||||
const buildDeleteURL = ( parent: ModelID, id: ModelID ) => buildChildURL( parent, id ) + '?force=true';
|
||||
|
||||
const delivery = createProductDeliveryTransformation();
|
||||
const inventory = createProductInventoryTransformation();
|
||||
const price = createProductPriceTransformation();
|
||||
const salesTax = createProductSalesTaxTransformation();
|
||||
const shipping = createProductShippingTransformation();
|
||||
const transformations = [
|
||||
...delivery,
|
||||
...inventory,
|
||||
...price,
|
||||
...salesTax,
|
||||
...shipping,
|
||||
];
|
||||
|
||||
const transformer = createProductDataTransformer<ProductVariation>( transformations );
|
||||
|
||||
return new ModelRepository(
|
||||
restListChild< ProductVariationRepositoryParams >( buildURL, ProductVariation, httpClient, transformer ),
|
||||
restCreateChild< ProductVariationRepositoryParams >( buildURL, ProductVariation, httpClient, transformer ),
|
||||
restReadChild< ProductVariationRepositoryParams >( buildChildURL, ProductVariation, httpClient, transformer ),
|
||||
restUpdateChild< ProductVariationRepositoryParams >( buildChildURL, ProductVariation, httpClient, transformer ),
|
||||
restDeleteChild< ProductVariationRepositoryParams >( buildDeleteURL, httpClient ),
|
||||
);
|
||||
}
|
|
@ -13,6 +13,7 @@ import {
|
|||
UpdateChildFn,
|
||||
DeleteChildFn,
|
||||
CreateFn,
|
||||
CreateChildFn,
|
||||
ModelTransformer,
|
||||
IgnorePropertyTransformation,
|
||||
// @ts-ignore
|
||||
|
@ -138,6 +139,31 @@ export function restCreate< T extends ModelRepositoryParams >(
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a callback for creating child models using the REST API.
|
||||
*
|
||||
* @param {Function} buildURL A callback to build the URL. (This is passed the properties for the new model.)
|
||||
* @param {Function} modelClass The model we're listing.
|
||||
* @param {HTTPClient} httpClient The HTTP client to use for the request.
|
||||
* @param {ModelTransformer} transformer The transformer to use for the response data.
|
||||
* @return {CreateChildFn} The callback for the repository.
|
||||
*/
|
||||
export function restCreateChild< T extends ModelRepositoryParams >(
|
||||
buildURL: ( parent: ParentID< T >, properties: Partial< ModelClass< T > > ) => string,
|
||||
modelClass: ModelConstructor< ModelClass< T > >,
|
||||
httpClient: HTTPClient,
|
||||
transformer: ModelTransformer< ModelClass< T > >,
|
||||
): CreateChildFn< T > {
|
||||
return async ( parent, properties ) => {
|
||||
const response = await httpClient.post(
|
||||
buildURL( parent, properties ),
|
||||
transformer.fromModel( properties ),
|
||||
);
|
||||
|
||||
return Promise.resolve( transformer.toModel( modelClass, response.data ) );
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a callback for reading models using the REST API.
|
||||
*
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
{
|
||||
"url": "http://localhost:8084/",
|
||||
"appName": "woocommerce_e2e",
|
||||
"users": {
|
||||
"admin": {
|
||||
"username": "admin",
|
||||
|
@ -16,8 +15,98 @@
|
|||
"name": "Simple product"
|
||||
},
|
||||
"variable": {
|
||||
"name": "Variable Product with Three Variations"
|
||||
"name": "Variable Product with Three Attributes",
|
||||
"defaultAttributes": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "Size",
|
||||
"option": "Medium"
|
||||
},
|
||||
{
|
||||
"id": 0,
|
||||
"name": "Colour",
|
||||
"option": "Blue"
|
||||
}
|
||||
],
|
||||
"attributes": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "Colour",
|
||||
"isVisibleOnProductPage": true,
|
||||
"isForVariations": true,
|
||||
"options": [
|
||||
"Red",
|
||||
"Green",
|
||||
"Blue"
|
||||
],
|
||||
"sortOrder": 0
|
||||
},
|
||||
{
|
||||
"id": 0,
|
||||
"name": "Size",
|
||||
"isVisibleOnProductPage": true,
|
||||
"isForVariations": true,
|
||||
"options": [
|
||||
"Small",
|
||||
"Medium",
|
||||
"Large"
|
||||
],
|
||||
"sortOrder": 0
|
||||
},
|
||||
{
|
||||
"id": 0,
|
||||
"name": "Logo",
|
||||
"isVisibleOnProductPage": true,
|
||||
"isForVariations": true,
|
||||
"options": [
|
||||
"Woo",
|
||||
"WordPress"
|
||||
],
|
||||
"sortOrder": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
"variations": [
|
||||
{
|
||||
"regularPrice": "19.99",
|
||||
"attributes": [
|
||||
{
|
||||
"name": "Size",
|
||||
"option": "Large"
|
||||
},
|
||||
{
|
||||
"name": "Colour",
|
||||
"option": "Red"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"regularPrice": "18.99",
|
||||
"attributes": [
|
||||
{
|
||||
"name": "Size",
|
||||
"option": "Medium"
|
||||
},
|
||||
{
|
||||
"name": "Colour",
|
||||
"option": "Green"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"regularPrice": "17.99",
|
||||
"attributes": [
|
||||
{
|
||||
"name": "Size",
|
||||
"option": "Small"
|
||||
},
|
||||
{
|
||||
"name": "Colour",
|
||||
"option": "Blue"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"grouped": {
|
||||
"name": "Grouped Product with Three Children",
|
||||
"groupedProducts": [
|
||||
|
|
|
@ -1,5 +1,12 @@
|
|||
# Unreleased
|
||||
|
||||
## Added
|
||||
|
||||
- api package test for variable products and product variations
|
||||
- api package test for grouped products
|
||||
- api package test for external products
|
||||
- api package test for coupons
|
||||
|
||||
# 0.1.1
|
||||
|
||||
## Added
|
||||
|
@ -14,6 +21,7 @@
|
|||
- Shopper Single Product tests
|
||||
- Shopper My Account Pay Order
|
||||
- Shopper Checkout Apply Coupon
|
||||
- Shopper Shop Browse Search Sort
|
||||
- Merchant Orders Customer Checkout Page
|
||||
- Shopper Cart Apply Coupon
|
||||
- Shopper Variable product info updates on different variations
|
||||
|
|
|
@ -71,6 +71,7 @@ The functions to access the core tests are:
|
|||
- `runMyAccountPayOrderTest` - Shopper can pay for his order in My Account
|
||||
- `runCartApplyCouponsTest` - Shopper can apply coupons in the cart
|
||||
- `runCheckoutApplyCouponsTest` - Shopper can apply coupons in the checkout
|
||||
- `runProductBrowseSearchSortTest` - Shopper can browse, search & sort products
|
||||
- `runVariableProductUpdateTest` - Shopper can view and update variations on a variable product
|
||||
|
||||
## Contributing a new test
|
||||
|
|
|
@ -57,7 +57,7 @@ const runGroupedProductAPITest = () => {
|
|||
expect( product ).toEqual( expect.objectContaining( baseGroupedProduct ) );
|
||||
});
|
||||
|
||||
it('can retrieve a raw external product', async () => {
|
||||
it('can retrieve a raw grouped product', async () => {
|
||||
let rawProperties = {
|
||||
id: product.id,
|
||||
grouped_products: baseGroupedProduct.groupedProducts,
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
/* eslint-disable jest/no-export, jest/no-disabled-tests */
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
const { HTTPClientFactory, VariableProduct, ProductVariation } = require( '@woocommerce/api' );
|
||||
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
const config = require( 'config' );
|
||||
const {
|
||||
it,
|
||||
describe,
|
||||
beforeAll,
|
||||
} = require( '@jest/globals' );
|
||||
|
||||
/**
|
||||
* Create a variable product and retrieve via the API.
|
||||
*/
|
||||
const runVariableProductAPITest = () => {
|
||||
describe('REST API > Variable Product', () => {
|
||||
let client;
|
||||
let defaultVariableProduct;
|
||||
let defaultVariations;
|
||||
let baseVariableProduct;
|
||||
let product;
|
||||
let variations = [];
|
||||
let productRepository;
|
||||
let variationRepository;
|
||||
|
||||
beforeAll(async () => {
|
||||
defaultVariableProduct = config.get('products.variable');
|
||||
defaultVariations = config.get('products.variations');
|
||||
const admin = config.get('users.admin');
|
||||
const url = config.get('url');
|
||||
|
||||
client = HTTPClientFactory.build(url)
|
||||
.withBasicAuth(admin.username, admin.password)
|
||||
.withIndexPermalinks()
|
||||
.create();
|
||||
});
|
||||
|
||||
it('can create a variable product', async () => {
|
||||
productRepository = VariableProduct.restRepository(client);
|
||||
|
||||
// Check properties of product in the create product response.
|
||||
product = await productRepository.create(defaultVariableProduct);
|
||||
expect(product).toEqual(expect.objectContaining(defaultVariableProduct));
|
||||
});
|
||||
|
||||
it('can add variations', async () => {
|
||||
variationRepository = ProductVariation.restRepository(client);
|
||||
for (let v = 0; v < defaultVariations.length; v++) {
|
||||
const variation = await variationRepository.create(product.id, defaultVariations[v]);
|
||||
// Test that variation id is a number.
|
||||
expect(variation.id).toBeGreaterThan(0);
|
||||
variations.push(variation.id);
|
||||
}
|
||||
|
||||
baseVariableProduct = {
|
||||
id: product.id,
|
||||
...defaultVariableProduct,
|
||||
variations,
|
||||
};
|
||||
});
|
||||
|
||||
it('can retrieve a transformed variable product', async () => {
|
||||
// Read product via the repository.
|
||||
const transformed = await productRepository.read(product.id);
|
||||
expect(transformed).toEqual(expect.objectContaining(baseVariableProduct));
|
||||
});
|
||||
|
||||
it('can retrieve transformed product variations', async () => {
|
||||
// Read variations via the repository.
|
||||
const transformed = await variationRepository.list(product.id);
|
||||
expect(transformed).toHaveLength(defaultVariations.length);
|
||||
});
|
||||
|
||||
it('can delete a variation', async () => {
|
||||
const variationId = baseVariableProduct.variations.pop();
|
||||
const status = variationRepository.delete(product.id, variationId);
|
||||
expect(status).toBeTruthy();
|
||||
});
|
||||
|
||||
it('can delete a variable product', async () => {
|
||||
const status = productRepository.delete(product.id);
|
||||
expect(status).toBeTruthy();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = runVariableProductAPITest;
|
|
@ -9,6 +9,7 @@ const { runOnboardingFlowTest, runTaskListTest } = require( './activate-and-setu
|
|||
const runInitialStoreSettingsTest = require( './activate-and-setup/setup.test' );
|
||||
|
||||
// Shopper tests
|
||||
const runProductBrowseSearchSortTest = require( './shopper/front-end-product-browse-search-sort.test' );
|
||||
const runCartApplyCouponsTest = require( './shopper/front-end-cart-coupons.test');
|
||||
const runCartPageTest = require( './shopper/front-end-cart.test' );
|
||||
const runCheckoutApplyCouponsTest = require( './shopper/front-end-checkout-coupons.test');
|
||||
|
@ -37,6 +38,7 @@ const runMerchantOrdersCustomerPaymentPage = require( './merchant/wp-admin-order
|
|||
const runExternalProductAPITest = require( './api/external-product.test' );
|
||||
const runCouponApiTest = require( './api/coupon.test' );
|
||||
const runGroupedProductAPITest = require( './api/grouped-product.test' );
|
||||
const runVariableProductAPITest = require( './api/variable-product.test' );
|
||||
|
||||
const runSetupOnboardingTests = () => {
|
||||
runActivationTest();
|
||||
|
@ -46,6 +48,7 @@ const runSetupOnboardingTests = () => {
|
|||
};
|
||||
|
||||
const runShopperTests = () => {
|
||||
runProductBrowseSearchSortTest();
|
||||
runCartApplyCouponsTest();
|
||||
runCartPageTest();
|
||||
runCheckoutApplyCouponsTest();
|
||||
|
@ -75,6 +78,7 @@ const runMerchantTests = () => {
|
|||
|
||||
const runApiTests = () => {
|
||||
runExternalProductAPITest();
|
||||
runVariableProductAPITest();
|
||||
runCouponApiTest();
|
||||
}
|
||||
|
||||
|
@ -86,6 +90,7 @@ module.exports = {
|
|||
runSetupOnboardingTests,
|
||||
runExternalProductAPITest,
|
||||
runGroupedProductAPITest,
|
||||
runVariableProductAPITest,
|
||||
runCouponApiTest,
|
||||
runCartApplyCouponsTest,
|
||||
runCartPageTest,
|
||||
|
@ -111,5 +116,6 @@ module.exports = {
|
|||
runProductSearchTest,
|
||||
runMerchantOrdersCustomerPaymentPage,
|
||||
runMerchantTests,
|
||||
runProductBrowseSearchSortTest,
|
||||
runApiTests,
|
||||
};
|
||||
|
|
|
@ -8,7 +8,8 @@ const {
|
|||
createCoupon,
|
||||
createSimpleProduct,
|
||||
uiUnblocked,
|
||||
clearAndFillInput,
|
||||
applyCoupon,
|
||||
removeCoupon,
|
||||
} = require( '@woocommerce/e2e-utils' );
|
||||
|
||||
/**
|
||||
|
@ -20,28 +21,6 @@ const {
|
|||
beforeAll,
|
||||
} = require( '@jest/globals' );
|
||||
|
||||
/**
|
||||
* Apply a coupon code to the cart.
|
||||
*
|
||||
* @param couponCode string
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const applyCouponToCart = async ( couponCode ) => {
|
||||
await clearAndFillInput('#coupon_code', couponCode);
|
||||
await expect(page).toClick('button', {text: 'Apply coupon'});
|
||||
await uiUnblocked();
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove one coupon from the cart.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const removeCouponFromCart = async () => {
|
||||
await expect(page).toClick('.woocommerce-remove-coupon', {text: '[Remove]'});
|
||||
await uiUnblocked();
|
||||
await expect(page).toMatchElement('.woocommerce-message', {text: 'Coupon has been removed.'});
|
||||
}
|
||||
const runCartApplyCouponsTest = () => {
|
||||
describe('Cart applying coupons', () => {
|
||||
let couponFixedCart;
|
||||
|
@ -62,42 +41,42 @@ const runCartApplyCouponsTest = () => {
|
|||
});
|
||||
|
||||
it('allows customer to apply fixed cart coupon', async () => {
|
||||
await applyCouponToCart( couponFixedCart );
|
||||
await applyCoupon(couponFixedCart);
|
||||
await expect(page).toMatchElement('.woocommerce-message', {text: 'Coupon code applied successfully.'});
|
||||
|
||||
// Verify discount applied and order total
|
||||
await page.waitForSelector('.order-total');
|
||||
await expect(page).toMatchElement('.cart-discount .amount', {text: '$5.00'});
|
||||
await expect(page).toMatchElement('.order-total .amount', {text: '$4.99'});
|
||||
await removeCouponFromCart();
|
||||
await removeCoupon(couponFixedCart);
|
||||
});
|
||||
|
||||
it('allows customer to apply percentage coupon', async () => {
|
||||
await applyCouponToCart( couponPercentage );
|
||||
await applyCoupon(couponPercentage);
|
||||
await expect(page).toMatchElement('.woocommerce-message', {text: 'Coupon code applied successfully.'});
|
||||
|
||||
// Verify discount applied and order total
|
||||
await page.waitForSelector('.order-total');
|
||||
await expect(page).toMatchElement('.cart-discount .amount', {text: '$4.99'});
|
||||
await expect(page).toMatchElement('.order-total .amount', {text: '$5.00'});
|
||||
await removeCouponFromCart();
|
||||
await removeCoupon(couponPercentage);
|
||||
});
|
||||
|
||||
it('allows customer to apply fixed product coupon', async () => {
|
||||
await applyCouponToCart( couponFixedProduct );
|
||||
await applyCoupon(couponFixedProduct);
|
||||
await expect(page).toMatchElement('.woocommerce-message', {text: 'Coupon code applied successfully.'});
|
||||
|
||||
// Verify discount applied and order total
|
||||
await page.waitForSelector('.order-total');
|
||||
await expect(page).toMatchElement('.cart-discount .amount', {text: '$5.00'});
|
||||
await expect(page).toMatchElement('.order-total .amount', {text: '$4.99'});
|
||||
await removeCouponFromCart();
|
||||
await removeCoupon(couponFixedProduct);
|
||||
});
|
||||
|
||||
it('prevents customer applying same coupon twice', async () => {
|
||||
await applyCouponToCart( couponFixedCart );
|
||||
await applyCoupon(couponFixedCart);
|
||||
await expect(page).toMatchElement('.woocommerce-message', {text: 'Coupon code applied successfully.'});
|
||||
await applyCouponToCart( couponFixedCart );
|
||||
await applyCoupon(couponFixedCart);
|
||||
// Verify only one discount applied
|
||||
// This is a work around for Puppeteer inconsistently finding 'Coupon code already applied'
|
||||
await expect(page).toMatchElement('.cart-discount .amount', {text: '$5.00'});
|
||||
|
@ -105,7 +84,7 @@ const runCartApplyCouponsTest = () => {
|
|||
});
|
||||
|
||||
it('allows customer to apply multiple coupons', async () => {
|
||||
await applyCouponToCart( couponFixedProduct );
|
||||
await applyCoupon(couponFixedProduct);
|
||||
await expect(page).toMatchElement('.woocommerce-message', {text: 'Coupon code applied successfully.'});
|
||||
|
||||
// Verify discount applied and order total
|
||||
|
@ -114,8 +93,8 @@ const runCartApplyCouponsTest = () => {
|
|||
});
|
||||
|
||||
it('restores cart total when coupons are removed', async () => {
|
||||
await removeCouponFromCart();
|
||||
await removeCouponFromCart();
|
||||
await removeCoupon(couponFixedCart);
|
||||
await removeCoupon(couponFixedProduct);
|
||||
await expect(page).toMatchElement('.order-total .amount', {text: '$9.99'});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,7 +8,8 @@ const {
|
|||
createCoupon,
|
||||
createSimpleProduct,
|
||||
uiUnblocked,
|
||||
clearAndFillInput,
|
||||
applyCoupon,
|
||||
removeCoupon,
|
||||
} = require( '@woocommerce/e2e-utils' );
|
||||
|
||||
/**
|
||||
|
@ -20,30 +21,6 @@ const {
|
|||
beforeAll,
|
||||
} = require( '@jest/globals' );
|
||||
|
||||
/**
|
||||
* Apply a coupon code to the cart.
|
||||
*
|
||||
* @param couponCode string
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const applyCouponToCart = async ( couponCode ) => {
|
||||
await expect(page).toClick('a', {text: 'Click here to enter your code'});
|
||||
await uiUnblocked();
|
||||
await clearAndFillInput('#coupon_code', couponCode);
|
||||
await expect(page).toClick('button', {text: 'Apply coupon'});
|
||||
await uiUnblocked();
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove one coupon from the cart.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const removeCouponFromCart = async () => {
|
||||
await expect(page).toClick('.woocommerce-remove-coupon', {text: '[Remove]'});
|
||||
await uiUnblocked();
|
||||
await expect(page).toMatchElement('.woocommerce-message', {text: 'Coupon has been removed.'});
|
||||
}
|
||||
const runCheckoutApplyCouponsTest = () => {
|
||||
describe('Checkout coupons', () => {
|
||||
let couponFixedCart;
|
||||
|
@ -64,7 +41,7 @@ const runCheckoutApplyCouponsTest = () => {
|
|||
});
|
||||
|
||||
it('allows customer to apply fixed cart coupon', async () => {
|
||||
await applyCouponToCart( couponFixedCart );
|
||||
await applyCoupon(couponFixedCart);
|
||||
await expect(page).toMatchElement('.woocommerce-message', {text: 'Coupon code applied successfully.'});
|
||||
|
||||
// Wait for page to expand total calculations to avoid flakyness
|
||||
|
@ -73,31 +50,31 @@ const runCheckoutApplyCouponsTest = () => {
|
|||
// Verify discount applied and order total
|
||||
await expect(page).toMatchElement('.cart-discount .amount', {text: '$5.00'});
|
||||
await expect(page).toMatchElement('.order-total .amount', {text: '$4.99'});
|
||||
await removeCouponFromCart();
|
||||
await removeCoupon(couponFixedCart);
|
||||
});
|
||||
|
||||
it('allows customer to apply percentage coupon', async () => {
|
||||
await applyCouponToCart( couponPercentage );
|
||||
await applyCoupon(couponPercentage);
|
||||
await expect(page).toMatchElement('.woocommerce-message', {text: 'Coupon code applied successfully.'});
|
||||
|
||||
// Verify discount applied and order total
|
||||
await expect(page).toMatchElement('.cart-discount .amount', {text: '$4.99'});
|
||||
await expect(page).toMatchElement('.order-total .amount', {text: '$5.00'});
|
||||
await removeCouponFromCart();
|
||||
await removeCoupon(couponPercentage);
|
||||
});
|
||||
|
||||
it('allows customer to apply fixed product coupon', async () => {
|
||||
await applyCouponToCart( couponFixedProduct );
|
||||
await applyCoupon(couponFixedProduct);
|
||||
await expect(page).toMatchElement('.woocommerce-message', {text: 'Coupon code applied successfully.'});
|
||||
await expect(page).toMatchElement('.cart-discount .amount', {text: '$5.00'});
|
||||
await expect(page).toMatchElement('.order-total .amount', {text: '$4.99'});
|
||||
await removeCouponFromCart();
|
||||
await removeCoupon(couponFixedProduct);
|
||||
});
|
||||
|
||||
it('prevents customer applying same coupon twice', async () => {
|
||||
await applyCouponToCart( couponFixedCart );
|
||||
await applyCoupon(couponFixedCart);
|
||||
await expect(page).toMatchElement('.woocommerce-message', {text: 'Coupon code applied successfully.'});
|
||||
await applyCouponToCart( couponFixedCart );
|
||||
await applyCoupon(couponFixedCart);
|
||||
// Verify only one discount applied
|
||||
// This is a work around for Puppeteer inconsistently finding 'Coupon code already applied'
|
||||
await expect(page).toMatchElement('.cart-discount .amount', {text: '$5.00'});
|
||||
|
@ -105,14 +82,14 @@ const runCheckoutApplyCouponsTest = () => {
|
|||
});
|
||||
|
||||
it('allows customer to apply multiple coupons', async () => {
|
||||
await applyCouponToCart( couponFixedProduct );
|
||||
await applyCoupon(couponFixedProduct);
|
||||
await expect(page).toMatchElement('.woocommerce-message', {text: 'Coupon code applied successfully.'});
|
||||
await expect(page).toMatchElement('.order-total .amount', {text: '$0.00'});
|
||||
});
|
||||
|
||||
it('restores cart total when coupons are removed', async () => {
|
||||
await removeCouponFromCart();
|
||||
await removeCouponFromCart();
|
||||
await removeCoupon(couponFixedCart);
|
||||
await removeCoupon(couponFixedProduct);
|
||||
await expect(page).toMatchElement('.order-total .amount', {text: '$9.99'});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
/* eslint-disable jest/no-export, jest/no-disabled-tests */
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
const {
|
||||
shopper,
|
||||
merchant,
|
||||
createSimpleProductWithCategory,
|
||||
uiUnblocked,
|
||||
} = require( '@woocommerce/e2e-utils' );
|
||||
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
const {
|
||||
it,
|
||||
describe,
|
||||
beforeAll,
|
||||
} = require( '@jest/globals' );
|
||||
|
||||
const config = require( 'config' );
|
||||
const simpleProductName = config.get( 'products.simple.name' );
|
||||
const singleProductPrice = config.has('products.simple.price') ? config.get('products.simple.price') : '9.99';
|
||||
const singleProductPrice2 = config.has('products.simple.price') ? config.get('products.simple.price') : '19.99';
|
||||
const singleProductPrice3 = config.has('products.simple.price') ? config.get('products.simple.price') : '29.99';
|
||||
const clothing = 'Clothing';
|
||||
const audio = 'Audio';
|
||||
const hardware = 'Hardware';
|
||||
const productTitle = 'li.first > a > h2.woocommerce-loop-product__title';
|
||||
|
||||
const runProductBrowseSearchSortTest = () => {
|
||||
describe('Search, browse by categories and sort items in the shop', () => {
|
||||
beforeAll(async () => {
|
||||
await merchant.login();
|
||||
// Create 1st product with Clothing category
|
||||
await createSimpleProductWithCategory(simpleProductName + ' 1', singleProductPrice, clothing);
|
||||
// Create 2nd product with Audio category
|
||||
await createSimpleProductWithCategory(simpleProductName + ' 2', singleProductPrice2, audio);
|
||||
// Create 3rd product with Hardware category
|
||||
await createSimpleProductWithCategory(simpleProductName + ' 3', singleProductPrice3, hardware);
|
||||
await merchant.logout();
|
||||
});
|
||||
|
||||
it('should let user search the store', async () => {
|
||||
await shopper.goToShop();
|
||||
await shopper.searchForProduct(simpleProductName + ' 1');
|
||||
});
|
||||
|
||||
it('should let user browse products by categories', async () => {
|
||||
// Browse through Clothing category link
|
||||
await Promise.all([
|
||||
page.waitForNavigation({waitUntil: 'networkidle0'}),
|
||||
page.click('span.posted_in > a', {text: clothing}),
|
||||
]);
|
||||
await uiUnblocked();
|
||||
|
||||
// Verify Clothing category page
|
||||
await page.waitForSelector(productTitle);
|
||||
await expect(page).toMatchElement(productTitle, {text: simpleProductName + ' 1'});
|
||||
await expect(page).toClick(productTitle, {text: simpleProductName + ' 1'});
|
||||
await uiUnblocked();
|
||||
await page.waitForSelector('h1.entry-title');
|
||||
await expect(page).toMatchElement('h1.entry-title', simpleProductName + ' 1');
|
||||
});
|
||||
|
||||
it('should let user sort the products in the shop', async () => {
|
||||
await shopper.goToShop();
|
||||
|
||||
// Sort by price high to low
|
||||
await page.select('.orderby', 'price-desc');
|
||||
// Verify the first product in sort order
|
||||
await expect(page).toMatchElement(productTitle, {text: simpleProductName + ' 3'});
|
||||
|
||||
// Sort by price low to high
|
||||
await page.select('.orderby', 'price');
|
||||
// Verify the first product in sort order
|
||||
await expect(page).toMatchElement(productTitle, {text: simpleProductName + ' 1'});
|
||||
|
||||
// Sort by date of creation, latest to oldest
|
||||
await page.select('.orderby', 'date');
|
||||
// Verify the first product in sort order
|
||||
await expect(page).toMatchElement(productTitle, {text: simpleProductName + ' 3'});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = runProductBrowseSearchSortTest;
|
|
@ -0,0 +1,6 @@
|
|||
/*
|
||||
* Internal dependencies
|
||||
*/
|
||||
const { runProductBrowseSearchSortTest } = require( '@woocommerce/e2e-core-tests' );
|
||||
|
||||
runProductBrowseSearchSortTest();
|
|
@ -0,0 +1,6 @@
|
|||
/*
|
||||
* Internal dependencies
|
||||
*/
|
||||
const { runVariableProductAPITest } = require( '@woocommerce/e2e-core-tests' );
|
||||
|
||||
runVariableProductAPITest();
|
|
@ -17,6 +17,9 @@
|
|||
- `createCoupon( couponAmount )` component which accepts a coupon amount string (it defaults to 5) and creates a basic coupon. Returns the generated coupon code.
|
||||
- `evalAndClick( selector )` use Puppeteer page.$eval to select and click and element.
|
||||
- `selectOptionInSelect2( selector, value )` util helper method that search and select in any select2 type field
|
||||
- `createSimpleProductWithCategory` component which creates a simple product with categories, containing three parameters for title, price and category name.
|
||||
- `applyCoupon( couponName )` util helper method which applies previously created coupon to cart or checkout
|
||||
- `removeCoupon()` util helper method that removes a single coupon within cart or checkout
|
||||
|
||||
## Changes
|
||||
|
||||
|
|
|
@ -77,6 +77,7 @@ describe( 'Cart page', () => {
|
|||
| `productIsInCheckout` | `productTitle, quantity, total, cartSubtotal` | Verify product is in cart on checkout page |
|
||||
| `removeFromCart` | `productTitle` | Remove a product from the cart on the cart page |
|
||||
| `setCartQuantity` | `productTitle, quantityValue` | Change the quantity of a product on the cart page |
|
||||
| `searchForProduct` | Searching for a product name and landing on its detail page |
|
||||
|
||||
### Page Utilities
|
||||
|
||||
|
@ -102,6 +103,8 @@ describe( 'Cart page', () => {
|
|||
| `moveAllItemsToTrash` | | Moves all items in a list view to the Trash |
|
||||
| `verifyAndPublish` | `noticeText` | Verify that an item can be published |
|
||||
| `selectOptionInSelect2` | `selector, value` | helper method that searchs for select2 type fields and select plus insert value inside
|
||||
| `applyCoupon` | `couponName` | helper method which applies a coupon in cart or checkout
|
||||
| `removeCoupon` | | helper method that removes a single coupon within cart or checkout
|
||||
|
||||
### Test Utilities
|
||||
|
||||
|
|
|
@ -184,6 +184,44 @@ const createSimpleProduct = async () => {
|
|||
return product.id;
|
||||
} ;
|
||||
|
||||
/**
|
||||
* Create simple product with categories
|
||||
*
|
||||
* @param productName Product's name which can be changed when writing a test
|
||||
* @param productPrice Product's price which can be changed when writing a test
|
||||
* @param categoryName Product's category which can be changed when writing a test
|
||||
*/
|
||||
const createSimpleProductWithCategory = async ( productName, productPrice, categoryName ) => {
|
||||
// Go to "add product" page
|
||||
await merchant.openNewProduct();
|
||||
|
||||
// Add title and regular price
|
||||
await expect(page).toFill('#title', productName);
|
||||
await expect(page).toClick('#_virtual');
|
||||
await clickTab('General');
|
||||
await expect(page).toFill('#_regular_price', productPrice);
|
||||
|
||||
// Try to select the existing category if present already, otherwise add a new and select it
|
||||
try {
|
||||
const [checkbox] = await page.$x('//label[contains(text(), "'+categoryName+'")]');
|
||||
await checkbox.click();
|
||||
} catch (error) {
|
||||
await expect(page).toClick('#product_cat-add-toggle');
|
||||
await expect(page).toFill('#newproduct_cat', categoryName);
|
||||
await expect(page).toClick('#product_cat-add-submit');
|
||||
}
|
||||
|
||||
// Publish the product
|
||||
await expect(page).toClick('#publish');
|
||||
await uiUnblocked();
|
||||
await page.waitForSelector('.updated.notice', {text:'Product published.'});
|
||||
|
||||
// Get the product ID
|
||||
const variablePostId = await page.$('#post_ID');
|
||||
let variablePostIdValue = (await(await variablePostId.getProperty('value')).jsonValue());
|
||||
return variablePostIdValue;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create variable product.
|
||||
*/
|
||||
|
@ -444,4 +482,5 @@ export {
|
|||
verifyAndPublish,
|
||||
addProductToOrder,
|
||||
createCoupon,
|
||||
createSimpleProductWithCategory,
|
||||
};
|
||||
|
|
|
@ -137,6 +137,17 @@ const shopper = {
|
|||
await quantityInput.type( quantityValue.toString() );
|
||||
},
|
||||
|
||||
searchForProduct: async ( prouductName ) => {
|
||||
await expect(page).toFill('.search-field', prouductName);
|
||||
await expect(page).toClick('.search-submit');
|
||||
await page.waitForSelector('h2.entry-title');
|
||||
await expect(page).toMatchElement('h2.entry-title', {text: prouductName});
|
||||
await expect(page).toClick('h2.entry-title', {text: prouductName});
|
||||
await page.waitForSelector('h1.entry-title');
|
||||
await expect(page.title()).resolves.toMatch(prouductName);
|
||||
await expect(page).toMatchElement('h1.entry-title', prouductName);
|
||||
},
|
||||
|
||||
/*
|
||||
* My Accounts flows.
|
||||
*/
|
||||
|
|
|
@ -209,6 +209,39 @@ const selectOptionInSelect2 = async ( value, selector = 'input.select2-search__f
|
|||
await page.keyboard.press('Enter');
|
||||
};
|
||||
|
||||
/**
|
||||
* Apply a coupon code within cart or checkout.
|
||||
* Method will try to apply a coupon in the checkout, otherwise will try to apply in the cart.
|
||||
*
|
||||
* @param couponCode string
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const applyCoupon = async ( couponCode ) => {
|
||||
try {
|
||||
await expect(page).toClick('a', {text: 'Click here to enter your code'});
|
||||
await uiUnblocked();
|
||||
await clearAndFillInput('#coupon_code', couponCode);
|
||||
await expect(page).toClick('button', {text: 'Apply coupon'});
|
||||
await uiUnblocked();
|
||||
} catch (error) {
|
||||
await clearAndFillInput('#coupon_code', couponCode);
|
||||
await expect(page).toClick('button', {text: 'Apply coupon'});
|
||||
await uiUnblocked();
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove one coupon within cart or checkout.
|
||||
*
|
||||
* @param couponCode Coupon name.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const removeCoupon = async ( couponCode ) => {
|
||||
await expect(page).toClick('[data-coupon="'+couponCode.toLowerCase()+'"]', {text: '[Remove]'});
|
||||
await uiUnblocked();
|
||||
await expect(page).toMatchElement('.woocommerce-message', {text: 'Coupon has been removed.'});
|
||||
};
|
||||
|
||||
export {
|
||||
clearAndFillInput,
|
||||
clickTab,
|
||||
|
@ -225,4 +258,6 @@ export {
|
|||
moveAllItemsToTrash,
|
||||
evalAndClick,
|
||||
selectOptionInSelect2,
|
||||
applyCoupon,
|
||||
removeCoupon,
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue