Merge pull request #30056 from woocommerce/add/e2e-shopper-create-products-api

Add/e2e shopper create products api
This commit is contained in:
Greg 2021-06-18 06:56:17 -06:00 committed by GitHub
commit 9e9a124f52
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 258 additions and 190 deletions

View File

@ -4,18 +4,37 @@
*/
const {
shopper,
merchant,
createSimpleProduct,
createVariableProduct,
createGroupedProduct,
uiUnblocked
} = require( '@woocommerce/e2e-utils' );
let simplePostIdValue;
let variablePostIdValue;
let groupedPostIdValue;
const config = require( 'config' );
// Variables for simple product
const simpleProductName = config.get( 'products.simple.name' );
let simplePostIdValue;
// Variables for variable product
const defaultVariableProduct = config.get( 'products.variable' );
let variableProductId;
// Variables for grouped product
const simpleProductPrice = config.has('products.simple.price') ? config.get('products.simple.price') : '9.99';
const simple1 = {
name: simpleProductName + ' 1',
regularPrice: simpleProductPrice
};
const simple2 = {
name: simpleProductName + ' 2',
regularPrice: simpleProductPrice
};
const groupedProduct = {
name: 'Grouped Product',
groupedProducts: [simple1, simple2]
};
let groupedPostIdValue;
const runSingleProductPageTest = () => {
describe('Single Product Page', () => {
@ -43,30 +62,34 @@ const runSingleProductPageTest = () => {
});
});
describe.skip('Variable Product Page', () => {
describe('Variable Product Page', () => {
beforeAll(async () => {
await merchant.login();
variablePostIdValue = await createVariableProduct();
await merchant.logout();
variableProductId = await createVariableProduct();
});
it('should be able to add variation products to the cart', async () => {
// Add a product with one set of variations to cart
await shopper.goToProduct(variablePostIdValue);
await expect(page).toSelect('#attr-1', 'val1');
await expect(page).toSelect('#attr-2', 'val1');
await expect(page).toSelect('#attr-3', 'val1');
await shopper.goToProduct(variableProductId);
for (const attr of defaultVariableProduct.attributes) {
const { name, options } = attr;
const selectElem = `#${name.toLowerCase()}`;
const value = options[0];
await expect(page).toSelect(selectElem, value);
}
await shopper.addToCart();
await expect(page).toMatchElement('.woocommerce-message', {text: 'has been added to your cart.'});
// Verify cart contents
await shopper.goToCart();
await shopper.productIsInCart('Variable Product with Three Variations');
await shopper.productIsInCart(defaultVariableProduct.name);
});
it('should be able to remove variation products from the cart', async () => {
// Remove items from cart
await shopper.removeFromCart('Variable Product with Three Variations');
await shopper.removeFromCart(defaultVariableProduct.name);
await uiUnblocked();
await expect(page).toMatchElement('.cart-empty', {text: 'Your cart is currently empty.'});
});
@ -74,9 +97,7 @@ const runSingleProductPageTest = () => {
describe('Grouped Product Page', () => {
beforeAll(async () => {
await merchant.login();
groupedPostIdValue = await createGroupedProduct();
await merchant.logout();
groupedPostIdValue = await createGroupedProduct(groupedProduct);
});
it('should be able to add grouped products to the cart', async () => {
@ -93,7 +114,7 @@ const runSingleProductPageTest = () => {
await quantityFields[1].type('5');
await shopper.addToCart();
await expect(page).toMatchElement('.woocommerce-message',
{text: '“'+simpleProductName+' 1” and “'+simpleProductName+' 2” have been added to your cart.'});
{text: '“'+simpleProductName+' 1” and “'+simpleProductName+' 2” have been added to your cart.'});
// Verify cart contents
await shopper.goToCart();

View File

@ -4,69 +4,99 @@
*/
const {
shopper,
merchant,
createVariableProduct,
} = require( '@woocommerce/e2e-utils' );
const config = require('config');
let variablePostIdValue;
const cartDialogMessage = 'Please select some product options before adding this product to your cart.';
const attributes = config.get( 'products.variable.attributes' )
const runVariableProductUpdateTest = () => {
describe('Shopper > Update variable product',() => {
beforeAll(async () => {
await merchant.login();
variablePostIdValue = await createVariableProduct();
await merchant.logout();
});
it('shopper can change variable attributes to the same value', async () => {
await shopper.goToProduct(variablePostIdValue);
await expect(page).toSelect('#attr-1', 'val1');
await expect(page).toSelect('#attr-2', 'val1');
await expect(page).toSelect('#attr-3', 'val1');
await expect(page).toMatchElement('.woocommerce-variation-price', { text: '9.99' });
for (const a of attributes) {
const { name, options } = a;
const attrHTMLId = `#${name.toLowerCase()}`;
await expect(page).toSelect(attrHTMLId, options[0]);
}
await expect(page).toMatchElement('.woocommerce-variation-price', {
text: '9.99'
});
});
it('shopper can change attributes to combination with dimensions and weight', async () => {
await shopper.goToProduct(variablePostIdValue);
await expect(page).toSelect('#attr-1', 'val1');
await expect(page).toSelect('#attr-2', 'val2');
await expect(page).toSelect('#attr-3', 'val1');
await expect(page).toSelect(
`#${attributes[0].name.toLowerCase()}`,
attributes[0].options[0]
);
await expect(page).toSelect(
`#${attributes[1].name.toLowerCase()}`,
attributes[1].options[1]
);
await expect(page).toSelect(
`#${attributes[2].name.toLowerCase()}`,
attributes[2].options[0]
);
await expect(page).toMatchElement('.woocommerce-variation-price', { text: '20.00' });
await expect(page).toMatchElement('.woocommerce-variation-availability', { text: 'Out of stock' });
await expect(page).toMatchElement('.woocommerce-product-attributes-item--weight', { text: '200 kg' });
await expect(page).toMatchElement('.woocommerce-product-attributes-item--dimensions', { text: '10 × 20 × 15 cm' });
});
it('shopper can change variable product attributes to variation with a different price', async () => {
await shopper.goToProduct(variablePostIdValue);
await expect(page).toSelect('#attr-1', 'val1');
await expect(page).toSelect('#attr-2', 'val1');
await expect(page).toSelect('#attr-3', 'val2');
await expect(page).toSelect(
`#${attributes[0].name.toLowerCase()}`,
attributes[0].options[0]
);
await expect(page).toSelect(
`#${attributes[1].name.toLowerCase()}`,
attributes[1].options[0]
);
await expect(page).toSelect(
`#${attributes[2].name.toLowerCase()}`,
attributes[2].options[1]
);
await expect(page).toMatchElement('.woocommerce-variation-price', { text: '11.99' });
});
it('shopper can reset variations', async () => {
await shopper.goToProduct(variablePostIdValue);
await expect(page).toSelect('#attr-1', 'val1');
await expect(page).toSelect('#attr-2', 'val2');
await expect(page).toSelect('#attr-3', 'val1');
await expect(page).toSelect(
`#${attributes[0].name.toLowerCase()}`,
attributes[0].options[0]
);
await expect(page).toSelect(
`#${attributes[1].name.toLowerCase()}`,
attributes[1].options[1]
);
await expect(page).toSelect(
`#${attributes[2].name.toLowerCase()}`,
attributes[2].options[0]
);
await expect(page).toClick('.reset_variations');
// Verify the reset by attempting to add the product to the cart
const couponDialog = await expect(page).toDisplayDialog(async () => {
await expect(page).toClick('.single_add_to_cart_button');
});
await expect(page).toClick('.single_add_to_cart_button');
});
expect(couponDialog.message()).toMatch(cartDialogMessage);
// Accept the dialog
await couponDialog.accept();
});
});

View File

@ -1,5 +1,9 @@
# Unreleased
## Added
- Factories for variable product, variation, and grouped product
# 0.1.5
## Added

View File

@ -7,10 +7,8 @@
*/
import { merchant, IS_RETEST_MODE } from './flows';
import {
clickTab,
uiUnblocked,
verifyCheckboxIsUnset,
selectOptionInSelect2,
setCheckbox,
unsetCheckbox,
evalAndClick,
@ -24,6 +22,8 @@ const client = factories.api.withDefaultPermalinks;
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');
/**
* Verify and publish
@ -223,172 +223,104 @@ const createSimpleProductWithCategory = async ( productName, productPrice, categ
/**
* Create variable product.
* Also, create variations for all attributes.
*
* @param varProduct Defaults to the variable product object in `default.json`
* @returns the ID of the created variable product
*/
const createVariableProduct = async () => {
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
// We need to remove any listeners on the `dialog` event otherwise we can't catch the dialogs below
page.removeAllListeners('dialog');
// Create variation for all attributes
const createVariation = (aIdx) => {
const { name, options } = attributes[aIdx];
const isLastAttribute = aIdx === attributes.length - 1;
// Go to "add product" page
await merchant.openNewProduct();
// Add each attribute value to the buffer.
options.forEach((opt) => {
buffer.push({
name: name,
option: opt
});
// Make sure we're on the add product page
await expect( page.title() ).resolves.toMatch( 'Add new product' );
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);
}
// Set product data
await expect( page ).toFill( '#title', 'Variable Product with Three Variations' );
await expect( page ).toSelect( '#product-type', 'Variable product' );
buffer.pop();
});
};
createVariation(aIdx);
// Create attributes for variations
await clickTab( 'Attributes' );
await expect( page ).toSelect( 'select[name="attribute_taxonomy"]', 'Custom product attribute' );
// Set some properties of 1st variation
variations[0].regularPrice = '9.99';
variations[0].virtual = true;
for ( let i = 0; i < 3; i++ ) {
await expect( page ).toClick( 'button.add_attribute', { text: 'Add' } );
// Wait for attribute form to load
await uiUnblocked();
// Set some properties of 2nd variation
variations[1].regularPrice = '11.99';
variations[1].virtual = true;
await page.focus( `input[name="attribute_names[${ i }]"]` );
await expect( page ).toFill( `input[name="attribute_names[${ i }]"]`, 'attr #' + ( i + 1 ) );
await expect( page ).toFill( `textarea[name="attribute_values[${ i }]"]`, 'val1 | val2' );
await expect( page ).toClick( `input[name="attribute_variation[${ i }]"]` );
// 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
});
}
await expect( page ).toClick( 'button', { text: 'Save attributes' } );
// Wait for attribute form to save (triggers 2 UI blocks)
await uiUnblocked();
await page.waitFor( 1000 );
await uiUnblocked();
// Create variations from attributes
await clickTab( 'Variations' );
await page.waitForSelector( 'select.variation_actions:not([disabled])' );
await page.focus( 'select.variation_actions' );
await expect( page ).toSelect( 'select.variation_actions', 'Create variations from all attributes' );
const firstDialog = await expect( page ).toDisplayDialog( async () => {
// Using this technique since toClick() isn't working.
// See: https://github.com/GoogleChrome/puppeteer/issues/1805#issuecomment-464802876
page.$eval( 'a.do_variation_action', elem => elem.click() );
} );
expect( firstDialog.message() ).toMatch( 'Are you sure you want to link all variations?' );
const secondDialog = await expect( page ).toDisplayDialog( async () => {
await firstDialog.accept();
} );
expect( secondDialog.message() ).toMatch( '8 variations added' );
await secondDialog.dismiss();
// Set some variation data
await uiUnblocked();
await uiUnblocked();
await page.waitForSelector( '.woocommerce_variation .handlediv' );
// Verify that variations were created
await Promise.all( [
expect( page ).toMatchElement( 'select[name="attribute_attr-1[0]"]', { text: 'val1' } ),
expect( page ).toMatchElement( 'select[name="attribute_attr-2[0]"]', { text: 'val1' } ),
expect( page ).toMatchElement( 'select[name="attribute_attr-3[0]"]', { text: 'val1' } ),
expect( page ).toMatchElement( 'select[name="attribute_attr-1[1]"]', { text: 'val1' } ),
expect( page ).toMatchElement( 'select[name="attribute_attr-2[1]"]', { text: 'val1' } ),
expect( page ).toMatchElement( 'select[name="attribute_attr-3[1]"]', { text: 'val2' } ),
expect( page ).toMatchElement( 'select[name="attribute_attr-1[2]"]', { text: 'val1' } ),
expect( page ).toMatchElement( 'select[name="attribute_attr-2[2]"]', { text: 'val2' } ),
expect( page ).toMatchElement( 'select[name="attribute_attr-3[2]"]', { text: 'val1' } ),
expect( page ).toMatchElement( 'select[name="attribute_attr-1[3]"]', { text: 'val1' } ),
expect( page ).toMatchElement( 'select[name="attribute_attr-2[3]"]', { text: 'val2' } ),
expect( page ).toMatchElement( 'select[name="attribute_attr-3[3]"]', { text: 'val2' } ),
expect( page ).toMatchElement( 'select[name="attribute_attr-1[4]"]', { text: 'val2' } ),
expect( page ).toMatchElement( 'select[name="attribute_attr-2[4]"]', { text: 'val1' } ),
expect( page ).toMatchElement( 'select[name="attribute_attr-3[4]"]', { text: 'val1' } ),
expect( page ).toMatchElement( 'select[name="attribute_attr-1[5]"]', { text: 'val2' } ),
expect( page ).toMatchElement( 'select[name="attribute_attr-2[5]"]', { text: 'val1' } ),
expect( page ).toMatchElement( 'select[name="attribute_attr-3[5]"]', { text: 'val2' } ),
expect( page ).toMatchElement( 'select[name="attribute_attr-1[6]"]', { text: 'val2' } ),
expect( page ).toMatchElement( 'select[name="attribute_attr-2[6]"]', { text: 'val2' } ),
expect( page ).toMatchElement( 'select[name="attribute_attr-3[6]"]', { text: 'val1' } ),
expect( page ).toMatchElement( 'select[name="attribute_attr-1[7]"]', { text: 'val2' } ),
expect( page ).toMatchElement( 'select[name="attribute_attr-2[7]"]', { text: 'val2' } ),
expect( page ).toMatchElement( 'select[name="attribute_attr-3[7]"]', { text: 'val2' } ),
] );
await expect( page ).toClick( '.woocommerce_variation:nth-of-type(2) .handlediv' );
await page.waitFor( 2000 );
await page.focus( 'input[name="variable_is_virtual[0]"]' );
await expect( page ).toClick( 'input[name="variable_is_virtual[0]"]' );
await expect( page ).toFill( 'input[name="variable_regular_price[0]"]', '9.99' );
await expect( page ).toClick( '.woocommerce_variation:nth-of-type(3) .handlediv' );
await page.waitFor( 2000 );
await page.focus( 'input[name="variable_is_virtual[1]"]' );
await expect( page ).toClick( 'input[name="variable_is_virtual[1]"]' );
await expect( page ).toFill( 'input[name="variable_regular_price[1]"]', '11.99' );
await expect( page ).toClick( '.woocommerce_variation:nth-of-type(4) .handlediv' );
await page.waitFor( 2000 );
await page.focus( 'input[name="variable_manage_stock[2]"]' );
await expect( page ).toClick( 'input[name="variable_manage_stock[2]"]' );
await expect( page ).toFill( 'input[name="variable_regular_price[2]"]', '20' );
await expect( page ).toFill( 'input[name="variable_weight[2]"]', '200' );
await expect( page ).toFill( 'input[name="variable_length[2]"]', '10' );
await expect( page ).toFill( 'input[name="variable_width[2]"]', '20' );
await expect( page ).toFill( 'input[name="variable_height[2]"]', '15' );
await page.focus( 'button.save-variation-changes' );
await expect( page ).toClick( 'button.save-variation-changes', { text: 'Save changes' } );
await verifyAndPublish( 'Product published.' );
const variablePostId = await page.$( '#post_ID' );
let variablePostIdValue = ( await ( await variablePostId.getProperty( 'value' ) ).jsonValue() );
return variablePostIdValue;
return id;
};
/**
* Create grouped product.
*
* @param groupedProduct Defaults to the grouped product object in `default.json`
* @returns ID of the grouped product
*/
const createGroupedProduct = async () => {
// Create two products to be linked in a grouped product after
await factories.products.simple.create( {
name: simpleProductName + ' 1',
regularPrice: simpleProductPrice
} );
await factories.products.simple.create( {
name: simpleProductName + ' 2',
regularPrice: simpleProductPrice
} );
const createGroupedProduct = async (groupedProduct = defaultGroupedProduct) => {
const { name, groupedProducts } = groupedProduct;
const simpleProductIds = [];
let groupedProductRequest;
// Go to "add product" page
await merchant.openNewProduct();
// 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);
}
// Make sure we're on the add product page
await expect( page.title() ).resolves.toMatch( 'Add new product' );
// Using the api, create the grouped product
groupedProductRequest = {
name: name,
groupedProducts: simpleProductIds
};
const { id } = await factories.products.grouped.create(
groupedProductRequest
);
// Set product data and save the product
await expect( page ).toFill( '#title', 'Grouped Product' );
await expect( page ).toSelect( '#product-type', 'Grouped product' );
await clickTab( 'Linked Products' );
await selectOptionInSelect2( simpleProductName + ' 1' );
await selectOptionInSelect2( simpleProductName + ' 2' );
await verifyAndPublish();
// Get product ID
const groupedPostId = await page.$( '#post_ID' );
let groupedPostIdValue = ( await ( await groupedPostId.getProperty( 'value' ) ).jsonValue() );
return groupedPostIdValue;
}
return id;
};
/**
* Create a basic order with the provided order status.

View File

@ -1,6 +1,9 @@
import { HTTPClientFactory } from '@woocommerce/api';
const config = require( 'config' );
import { simpleProductFactory } from './factories/simple-product';
import { variableProductFactory } from './factories/variable-product';
import { variationFactory } from './factories/variation';
import { groupedProductFactory } from './factories/grouped-product';
const apiUrl = config.get( 'url' );
const adminUsername = config.get( 'users.admin.username' );
@ -20,6 +23,9 @@ const factories = {
},
products: {
simple: simpleProductFactory( withDefaultPermalinks ),
variable: variableProductFactory( withDefaultPermalinks ),
variation: variationFactory( withDefaultPermalinks ),
grouped: groupedProductFactory( withDefaultPermalinks )
},
};

View File

@ -0,0 +1,26 @@
import { GroupedProduct } from '@woocommerce/api';
import { Factory } from 'fishery';
/**
* Creates a new factory for creating variable products.
* This does not include creating product variations.
* Instead, use `variationFactory()` for that.
*
* @param {HTTPClient} httpClient The HTTP client we will give the repository.
* @return {AsyncFactory} The factory for creating models.
*/
export function groupedProductFactory(httpClient) {
const repository = GroupedProduct.restRepository(httpClient);
return Factory.define(({ params, onCreate }) => {
onCreate((model) => {
return repository.create(model);
});
return {
name: params.name,
type: 'grouped',
groupedProducts: params.groupedProducts
};
});
}

View File

@ -0,0 +1,27 @@
import { VariableProduct } from '@woocommerce/api';
import { Factory } from 'fishery';
/**
* Creates a new factory for creating variable products.
* This does not include creating product variations.
* Instead, use `variationFactory()` for that.
*
* @param {HTTPClient} httpClient The HTTP client we will give the repository.
* @return {AsyncFactory} The factory for creating models.
*/
export function variableProductFactory(httpClient) {
const repository = VariableProduct.restRepository(httpClient);
return Factory.define(({ params, onCreate }) => {
onCreate((model) => {
return repository.create(model);
});
return {
name: params.name,
type: 'variable',
defaultAttributes: params.defaultAttributes,
attributes: params.attributes
};
});
}

View File

@ -0,0 +1,22 @@
import { ProductVariation } from '@woocommerce/api';
import { Factory } from 'fishery';
/**
* Creates a new factory for creating a product variation.
*
* @param {HTTPClient} httpClient The HTTP client we will give the repository.
* @return {AsyncFactory} The factory for creating models.
*/
export function variationFactory(httpClient) {
const repository = ProductVariation.restRepository(httpClient);
return Factory.define(({ params, onCreate }) => {
const { productId, variation } = params;
onCreate((model) => {
return repository.create(productId, model);
});
return variation;
});
}