woocommerce/packages/js/e2e-utils/src/components.js

713 lines
20 KiB
JavaScript
Raw Normal View History

/**
* Internal dependencies
*/
2021-04-09 19:15:00 +00:00
import { merchant, IS_RETEST_MODE } from './flows';
import {
uiUnblocked,
verifyCheckboxIsUnset,
setCheckbox,
2021-04-08 10:41:02 +00:00
unsetCheckbox,
evalAndClick,
2021-04-26 15:18:40 +00:00
backboneUnblocked,
2021-04-27 18:41:50 +00:00
waitForSelectorWithoutThrow,
} from './page-utils';
import factories from './factories';
2021-09-29 21:57:33 +00:00
import { waitForTimeout } from './flows/utils';
2021-11-01 20:40:03 +00:00
import { withRestApi } from './flows/with-rest-api';
2021-08-20 18:52:29 +00:00
import { Coupon, Order } from '@woocommerce/api';
const client = factories.api.withDefaultPermalinks;
2019-12-06 15:06:48 +00:00
const config = require( 'config' );
const simpleProductName = config.get( 'products.simple.name' );
const simpleProductPrice = config.has( 'products.simple.price' )
? config.get( 'products.simple.price' )
: '9.99';
const defaultVariableProduct = config.get( 'products.variable' );
const defaultGroupedProduct = config.get( 'products.grouped' );
2019-12-06 15:06:48 +00:00
const uuid = require( 'uuid' );
/**
* Verify and publish
*
* @param {string} noticeText The text that appears in the notice after publishing.
*/
const verifyAndPublish = async ( noticeText ) => {
// Wait for auto save
2021-09-29 21:57:33 +00:00
await waitForTimeout( 2000 );
// Publish product
await expect( page ).toClick( '#publish' );
await page.waitForSelector( '.updated.notice' );
// Verify
await expect( page ).toMatchElement( '.updated.notice', {
text: noticeText,
} );
};
/**
* Wait for primary button to be enabled and click.
*
* @param {boolean} waitForNetworkIdle - Wait for network idle after click
* @return {Promise<void>}
*/
const waitAndClickPrimary = async ( waitForNetworkIdle = true ) => {
// Wait for "Continue" button to become active
await page.waitForSelector( 'button.is-primary:not(:disabled)' );
await page.click( 'button.is-primary' );
if ( waitForNetworkIdle ) {
await page.waitForNavigation( { waitUntil: 'networkidle0' } );
}
};
/**
* Complete onboarding wizard.
*/
const completeOnboardingWizard = async () => {
// Store Details section
2021-04-27 18:41:50 +00:00
await merchant.runSetupWizard();
// Fill store's address - first line
await expect( page ).toFill(
'#inspector-text-control-0',
config.get( 'addresses.admin.store.addressfirstline' )
);
// Fill store's address - second line
await expect( page ).toFill(
'#inspector-text-control-1',
config.get( 'addresses.admin.store.addresssecondline' )
);
// Fill country and state where the store is located
await expect( page ).toFill(
'.woocommerce-select-control__control-input',
config.get( 'addresses.admin.store.countryandstate' )
);
// Fill the city where the store is located
await expect( page ).toFill(
'#inspector-text-control-2',
config.get( 'addresses.admin.store.city' )
);
// Fill postcode of the store
await expect( page ).toFill(
'#inspector-text-control-3',
config.get( 'addresses.admin.store.postcode' )
);
// Verify that checkbox next to "I'm setting up a store for a client" is not selected
await verifyCheckboxIsUnset( '.components-checkbox-control__input' );
// Wait for "Continue" button to become active
await page.waitForSelector( 'button.is-primary:not(:disabled)' );
// Click on "Continue" button to move to the next step
await page.click( 'button.is-primary', { text: 'Continue' } );
2021-04-09 19:15:00 +00:00
// Wait for usage tracking pop-up window to appear on a new site
const usageTrackingHeader = await page.$(
'.components-modal__header-heading'
);
2021-04-27 18:41:50 +00:00
if ( usageTrackingHeader ) {
await expect( page ).toMatchElement(
'.components-modal__header-heading',
{
text: 'Build a better WooCommerce',
}
2021-04-09 19:15:00 +00:00
);
// Query for "No Thanks" buttons
const continueButtons = await page.$$(
'.woocommerce-usage-modal__actions button.is-secondary'
);
expect( continueButtons ).toHaveLength( 1 );
await continueButtons[ 0 ].click();
await expect( page ).toMatchElement(
'.woocommerce-usage-modal__actions button.is-secondary.is-busy'
);
await expect( page ).not.toMatchElement(
'.woocommerce-usage-modal__actions button.is-primary:disabled'
);
2021-04-09 19:15:00 +00:00
}
await page.waitForNavigation( { waitUntil: 'networkidle0' } );
// Industry section
// Query for the industries checkboxes
const industryCheckboxes = await page.$$(
'.components-checkbox-control__input'
);
2020-09-30 19:00:14 +00:00
expect( industryCheckboxes ).toHaveLength( 8 );
// Select all industries including "Other"
2020-09-30 19:00:14 +00:00
for ( let i = 0; i < 8; i++ ) {
await industryCheckboxes[ i ].click();
}
// Fill "Other" industry
await expect( page ).toFill(
'.components-text-control__input',
config.get( 'onboardingwizard.industry' )
);
// Wait for "Continue" button to become active
await waitAndClickPrimary();
// Product types section
// Query for the product types checkboxes
const productTypesCheckboxes = await page.$$(
'.components-checkbox-control__input'
);
2020-09-02 23:14:25 +00:00
expect( productTypesCheckboxes ).toHaveLength( 7 );
// Select Physical and Downloadable products
for ( let i = 1; i < 2; i++ ) {
await productTypesCheckboxes[ i ].click();
}
// Wait for "Continue" button to become active
await waitAndClickPrimary();
// Business Details section
// Temporarily add delay to reduce test flakiness
await page.waitFor( 2000 );
// Query for the <SelectControl>s
const selectControls = await page.$$( '.woocommerce-select-control' );
expect( selectControls ).toHaveLength( 2 );
// Fill the number of products you plan to sell
await selectControls[ 0 ].click();
await page.waitForSelector( '.woocommerce-select-control__control' );
await expect( page ).toClick( '.woocommerce-select-control__option', {
text: config.get( 'onboardingwizard.numberofproducts' ),
} );
// Fill currently selling elsewhere
await selectControls[ 1 ].click();
await page.waitForSelector( '.woocommerce-select-control__control' );
await expect( page ).toClick( '.woocommerce-select-control__option', {
text: config.get( 'onboardingwizard.sellingelsewhere' ),
} );
// Wait for "Continue" button to become active
await waitAndClickPrimary( false );
// Skip installing extensions
await unsetCheckbox( '.components-checkbox-control__input' );
await verifyCheckboxIsUnset( '.components-checkbox-control__input' );
await waitAndClickPrimary();
// Theme section
await waitAndClickPrimary();
// End of onboarding wizard
if ( IS_RETEST_MODE ) {
2021-04-09 19:15:00 +00:00
// Home screen modal can't be reset via the rest api.
return;
}
2020-08-21 15:03:02 +00:00
// Wait for homescreen welcome modal to appear
const welcomeHeader = await waitForSelectorWithoutThrow(
'.woocommerce__welcome-modal__page-content'
);
2021-04-27 18:41:50 +00:00
if ( ! welcomeHeader ) {
return;
}
2020-08-21 15:03:02 +00:00
2021-04-27 18:41:50 +00:00
// Click two Next buttons
for ( let b = 0; b < 2; b++ ) {
await page.waitForSelector( 'button.components-guide__forward-button' );
await page.click( 'button.components-guide__forward-button' );
2021-04-27 18:41:50 +00:00
}
2020-08-21 15:03:02 +00:00
// Wait for "Let's go" button to become active
await page.waitForSelector( 'button.components-guide__finish-button' );
await page.click( 'button.components-guide__finish-button' );
};
/**
* Create simple product.
*
* @param {string} productTitle Defaults to Simple Product. Customizable title.
* @param {string} productPrice Defaults to $9.99. Customizable pricing.
* @param {Object} additionalProps Defaults to nothing. Additional product properties.
*/
const createSimpleProduct = async (
productTitle = simpleProductName,
productPrice = simpleProductPrice,
additionalProps = {}
) => {
const newProduct = {
name: productTitle,
regularPrice: productPrice,
...additionalProps,
};
const product = await factories.products.simple.create( newProduct );
return product.id;
};
/**
* Create simple product with categories
*
* @param {string} productName Product's name which can be changed when writing a test
* @param {string} productPrice Product's price which can be changed when writing a test
* @param {string} categoryName Product's category which can be changed when writing a test
*/
const createSimpleProductWithCategory = async (
productName,
productPrice,
categoryName
) => {
// Get the category ID so we can add it to the product below
const categoryId = await withRestApi.createProductCategory( categoryName );
const product = await factories.products.simple.create( {
name: productName,
regularPrice: productPrice,
categories: [
{
id: categoryId,
},
],
isVirtual: true,
} );
return product.id;
};
/**
* Create simple downloadable product
*
* @param {string} name Product's name. Defaults to 'Simple Product' (see createSimpleProduct definition).
* @param {number} downloadLimit Product's download limit. Defaults to '-1' (unlimited).
* @param {string} downloadName Product's download name. Defaults to 'Single'.
* @param {string} price Product's price. Defaults to '$9.99' (see createSimpleProduct definition).
*/
const createSimpleDownloadableProduct = async (
name,
downloadLimit = -1,
downloadName = 'Single',
price
) => {
const productDownloadDetails = {
downloadable: true,
downloads: [
{
id: uuid.v4(),
name: downloadName,
file:
'https://demo.woothemes.com/woocommerce/wp-content/uploads/sites/56/2017/08/single.jpg',
},
],
download_limit: downloadLimit,
};
return await createSimpleProduct( name, price, productDownloadDetails );
};
/**
* Create variable product.
2021-06-07 08:11:41 +00:00
* Also, create variations for all attributes.
*
* @param {Object} varProduct Defaults to the variable product object in `default.json`
* @return {number} the ID of the created variable product
*/
const createVariableProduct = async ( varProduct = defaultVariableProduct ) => {
const { attributes } = varProduct;
const { id } = await factories.products.variable.create( varProduct ); // create the variable product
const variations = [];
const buffer = []; // accumulated attributes while looping
const aIdx = 0; // attributes[] index
// Create variation for all attributes
// eslint-disable-next-line no-shadow
const createVariation = ( aIdx ) => {
const { name, options } = attributes[ aIdx ];
const isLastAttribute = aIdx === attributes.length - 1;
// Add each attribute value to the buffer.
options.forEach( ( opt ) => {
buffer.push( {
name,
option: opt,
} );
if ( isLastAttribute ) {
// If this is the last attribute, it means the variation is now complete.
// Save whatever's been accumulated in the buffer to the `variations[]` array.
variations.push( {
attributes: [ ...buffer ],
} );
} else {
// Otherwise, move to the next attribute first
// before proceeding to the next value in this attribute.
createVariation( aIdx + 1 );
}
buffer.pop();
} );
};
createVariation( aIdx );
// Set some properties of 1st variation
variations[ 0 ].regularPrice = '9.99';
variations[ 0 ].virtual = true;
// Set some properties of 2nd variation
variations[ 1 ].regularPrice = '11.99';
variations[ 1 ].virtual = true;
// Set some properties of 3rd variation
variations[ 2 ].regularPrice = '20';
variations[ 2 ].weight = '200';
variations[ 2 ].dimensions = {
length: '10',
width: '20',
height: '15',
};
variations[ 2 ].manage_stock = true;
// Use API to create each variation
for ( const v of variations ) {
await factories.products.variation.create( {
productId: id,
variation: v,
} );
}
return id;
};
/**
* Create grouped product.
*
* @param {Object} groupedProduct Defaults to the grouped product object in `default.json`
* @return {number} ID of the grouped product
*/
const createGroupedProduct = async (
groupedProduct = defaultGroupedProduct
) => {
const { name, groupedProducts } = groupedProduct;
const simpleProductIds = [];
let groupedProductRequest;
// Using the api, create simple products to be grouped
for ( const simpleProduct of groupedProducts ) {
const { id } = await factories.products.simple.create( simpleProduct );
simpleProductIds.push( id );
}
// Using the api, create the grouped product
// eslint-disable-next-line prefer-const
groupedProductRequest = {
name,
groupedProducts: simpleProductIds,
};
const { id } = await factories.products.grouped.create(
groupedProductRequest
);
2021-06-07 09:41:36 +00:00
return id;
};
/**
* Use the API to create an order with the provided details.
*
* @param {Object} orderOptions
* @return {Promise<number>} ID of the created order.
*/
const createOrder = async ( orderOptions = {} ) => {
2021-08-20 18:52:29 +00:00
const newOrder = {
...( orderOptions.status && { status: orderOptions.status } ),
...( orderOptions.customerId && {
customer_id: orderOptions.customerId,
} ),
...( orderOptions.customerBilling && {
billing: orderOptions.customerBilling,
} ),
...( orderOptions.customerShipping && {
shipping: orderOptions.customerShipping,
} ),
...( orderOptions.productId && {
line_items: [ { product_id: orderOptions.productId } ],
} ),
...( orderOptions.lineItems && {
line_items: orderOptions.lineItems,
} ),
2021-08-20 18:52:29 +00:00
};
2021-08-20 18:52:29 +00:00
const repository = Order.restRepository( client );
const order = await repository.create( newOrder );
2021-08-20 18:52:29 +00:00
return order.id;
};
/**
* Create a basic order with the provided order status.
*
* @param {string} orderStatus Status of the new order. Defaults to `Pending payment`.
*/
const createSimpleOrder = async ( orderStatus = 'Pending payment' ) => {
// Go to 'Add new order' page
await merchant.openNewOrder();
// Make sure we're on the add order page
await expect( page.title() ).resolves.toMatch( 'Add new order' );
// Set order status
await expect( page ).toSelect( '#order_status', orderStatus );
// Wait for auto save
await page.waitFor( 2000 );
// Create the order
await expect( page ).toClick( 'button.save_order' );
await page.waitForSelector( '#message' );
// Verify
await expect( page ).toMatchElement( '#message', {
text: 'Order updated.',
} );
const variablePostId = await page.$( '#post_ID' );
const variablePostIdValue = await (
await variablePostId.getProperty( 'value' )
).jsonValue();
return variablePostIdValue;
};
/**
* Creates a batch of orders from the given `statuses`
* using the "Batch Create Order" API.
2021-09-29 21:57:33 +00:00
*
* @param {Array} statuses Array of order statuses
*/
const batchCreateOrders = async ( statuses ) => {
const defaultOrder = config.get( 'orders.basicPaidOrder' );
const path = '/wc/v3/orders/batch';
// Create an order per status
const orders = statuses.map( ( s ) => {
return {
...defaultOrder,
status: s,
};
} );
// Set the request payload from the created orders.
// Then send the API request.
const payload = { create: orders };
const response = await client.post( path, payload );
expect( response.status ).toEqual( 200 );
};
/**
* Adds a product to an order in the merchant.
*
* @param {number} orderId ID of the order to add the product to.
* @param {string} productName Name of the product being added to the order.
*/
const addProductToOrder = async ( orderId, productName ) => {
await merchant.goToOrder( orderId );
// Add a product to the order
await expect( page ).toClick( 'button.add-line-item' );
await expect( page ).toClick( 'button.add-order-item' );
await page.waitForSelector( '.wc-backbone-modal-header' );
await expect( page ).toClick(
'.wc-backbone-modal-content .wc-product-search'
);
await expect( page ).toFill(
'#wc-backbone-modal-dialog + .select2-container .select2-search__field',
productName
);
await page.waitForSelector( 'li[aria-selected="true"]', {
timeout: 10000,
} );
await expect( page ).toClick( 'li[aria-selected="true"]' );
await page.click( '.wc-backbone-modal-content #btn-ok' );
2021-04-26 15:18:40 +00:00
await backboneUnblocked();
// Verify the product we added shows as a line item now
await expect( page ).toMatchElement( '.wc-order-item-name', {
text: productName,
} );
};
/**
* Creates a basic coupon with the provided coupon amount. Returns the coupon code.
*
* @param {string} couponAmount Amount to be applied. Defaults to 5.
* @param {string} discountType Type of a coupon. Defaults to Fixed cart discount.
*/
const createCoupon = async (
couponAmount = '5',
discountType = 'Fixed cart discount'
) => {
let couponType;
switch ( discountType ) {
case 'Fixed cart discount':
couponType = 'fixed_cart';
break;
case 'Fixed product discount':
couponType = 'fixed_product';
break;
case 'Percentage discount':
couponType = 'percent';
break;
default:
couponType = discountType;
}
// Fill in coupon code
const couponCode = 'code-' + couponType + new Date().getTime().toString();
const repository = Coupon.restRepository( client );
await repository.create( {
code: couponCode,
discountType: couponType,
amount: couponAmount,
} );
return couponCode;
};
/**
* Adds a shipping zone along with a shipping method.
*
* @param {string} zoneName Shipping zone name.
* @param {string} zoneLocation Shiping zone location. Defaults to country:US. For states use: state:US:CA
* @param {string} zipCode Shipping zone zip code. Defaults to empty one space.
* @param {string} zoneMethod Shipping method type. Defaults to flat_rate (use also: free_shipping or local_pickup)
*/
const addShippingZoneAndMethod = async (
zoneName,
zoneLocation = 'country:US',
zipCode = ' ',
zoneMethod = 'flat_rate'
) => {
await merchant.openNewShipping();
// Fill shipping zone name
await page.waitForSelector( 'input#zone_name' );
await expect( page ).toFill( 'input#zone_name', zoneName );
// Select shipping zone location
await expect( page ).toSelect(
'select[name="zone_locations"]',
zoneLocation
);
2021-08-25 22:15:06 +00:00
await uiUnblocked();
2021-02-12 16:43:01 +00:00
// Fill shipping zone postcode if needed otherwise just put empty space
await page.waitForSelector( 'a.wc-shipping-zone-postcodes-toggle' );
await expect( page ).toClick( 'a.wc-shipping-zone-postcodes-toggle' );
await expect( page ).toFill( '#zone_postcodes', zipCode );
await expect( page ).toMatchElement( '#zone_postcodes', zipCode );
await expect( page ).toClick( 'button#submit' );
2021-02-12 16:43:01 +00:00
2021-08-25 22:15:06 +00:00
await uiUnblocked();
// Add shipping zone method
await page.waitFor( 1000 );
await expect( page ).toClick( 'button.wc-shipping-zone-add-method', {
text: 'Add shipping method',
} );
await page.waitForSelector( '.wc-shipping-zone-method-selector' );
await expect( page ).toSelect( 'select[name="add_method_id"]', zoneMethod );
await expect( page ).toClick( 'button#btn-ok' );
await page.waitForSelector( '#zone_locations' );
2021-08-25 22:15:06 +00:00
await uiUnblocked();
};
/**
2021-03-03 17:49:06 +00:00
* Click the Update button on the order details page.
*
* @param {string} noticeText The text that appears in the notice after updating the order.
* @param {boolean} waitForSave Optionally wait for auto save.
*/
const clickUpdateOrder = async ( noticeText, waitForSave = false ) => {
if ( waitForSave ) {
await page.waitFor( 2000 );
}
// Update order
await expect( page ).toClick( 'button.save_order' );
await page.waitForSelector( '.updated.notice' );
// Verify
await expect( page ).toMatchElement( '.updated.notice', {
text: noticeText,
} );
};
/**
* Delete all email logs in the WP Mail Logging plugin page.
*/
const deleteAllEmailLogs = async () => {
await merchant.openEmailLog();
// Make sure we have emails to delete. If we don't, this selector will return null.
if ( ( await page.$( '#bulk-action-selector-top' ) ) !== null ) {
await setCheckbox( '#cb-select-all-1' );
await expect( page ).toSelect( '#bulk-action-selector-top', 'Delete' );
await Promise.all( [
page.click( '#doaction' ),
page.waitForNavigation( { waitUntil: 'networkidle0' } ),
] );
}
};
2021-04-08 10:41:02 +00:00
/**
* Delete all the existing shipping zones.
*/
const deleteAllShippingZones = async () => {
await merchant.openSettings( 'shipping' );
2021-04-08 13:09:59 +00:00
2021-04-08 10:41:02 +00:00
// Delete existing shipping zones.
try {
let zone = await page.$( '.wc-shipping-zone-delete' );
if ( zone ) {
// WP action links aren't clickable because they are hidden with a left=-9999 style.
await page.evaluate( () => {
document.querySelector(
'.wc-shipping-zone-name .row-actions'
).style.left = '0';
} );
2021-04-08 10:41:02 +00:00
while ( zone ) {
await evalAndClick( '.wc-shipping-zone-delete' );
await uiUnblocked();
zone = await page.$( '.wc-shipping-zone-delete' );
}
}
} catch ( error ) {
2021-04-08 10:41:02 +00:00
// Prevent an error here causing the test to fail.
}
2021-04-08 10:41:02 +00:00
};
export {
completeOnboardingWizard,
createSimpleProduct,
createVariableProduct,
createGroupedProduct,
createSimpleOrder,
verifyAndPublish,
addProductToOrder,
createCoupon,
addShippingZoneAndMethod,
createSimpleProductWithCategory,
createSimpleDownloadableProduct,
2021-03-07 14:46:03 +00:00
clickUpdateOrder,
deleteAllEmailLogs,
2021-04-08 10:41:02 +00:00
deleteAllShippingZones,
batchCreateOrders,
2021-08-20 18:52:29 +00:00
createOrder,
};