[e2e] Product Editor: Add SKU, GTIN, shipping, and custom fields to simple product creation test (#50241)

* Refactor to add steps in 'can create a simple product' test

* Remove unnecessary await

* Fill in shipping dimensions

* Add frontend verification step

* Get permalink from editor page

* Verify shipping dimensions

* Verify sku and gtin

* Fix label association for sku field

* Fill in shipping class

* Fill custom field

* Handle SKU auto-update before filling in SKU

* Changelog

* Changelog

* Add more specific locator for attributes "Add new" button

* Update comment for why click() is used instead of check() for toggle

* Fix incorrect selector for attributes "Add new" button

* Remove check for toggle being checked, as it is flaky

* Await isChecked()

* Scroll custom fields toggle into view if needed

* Make custom field toggling more reliable

* Break filling into a separate step

* Break filling and verifying of full description into separate steps

* Verify the description and fix verifying shipping details

* Make showing of description toolbar more reliable

* Suppress errant warning

* Suppress warning for conditional and add comment about why it is okay

* Remove unused const
This commit is contained in:
Matt Sherman 2024-08-07 03:34:31 -04:00 committed by GitHub
parent 7fd864d234
commit fe7fafa729
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 412 additions and 141 deletions

View File

@ -0,0 +1,4 @@
Significance: patch
Type: fix
Fix association of label and input for SKU field.

View File

@ -3,6 +3,7 @@
*/
import { __ } from '@wordpress/i18n';
import { BlockAttributes } from '@wordpress/blocks';
import { useInstanceId } from '@wordpress/compose';
import { createElement, createInterpolateElement } from '@wordpress/element';
import { useWooBlockProps } from '@woocommerce/block-templates';
import { Product } from '@woocommerce/data';
@ -46,10 +47,15 @@ export function Edit( {
[ sku ]
);
const inputControlId = useInstanceId(
BaseControl,
'product_sku'
) as string;
return (
<div { ...blockProps }>
<BaseControl
id={ 'product_sku' }
id={ inputControlId }
className="woocommerce-product-form_inventory-sku"
label={ createInterpolateElement(
__( 'Sku <description />', 'woocommerce' ),
@ -64,6 +70,7 @@ export function Edit( {
>
<InputControl
ref={ skuRef }
id={ inputControlId }
name={ 'woocommerce-product-sku' }
onChange={ setSku }
value={ sku || '' }

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Add additional fields to new product editor e2e tests.

View File

@ -20,6 +20,18 @@ const productData = {
descriptionSimple: 'This is a product simple description',
productPrice: '100',
salePrice: '90',
customFields: [
{ name: `custom-field_${ Date.now() }`, value: 'custom_1' },
],
sku: `sku_${ Date.now() }`,
gtin: `gtin_${ Date.now() }`,
shipping: {
shippingClassName: `shipping-class_${ Date.now() }`,
weight: '2',
length: '20',
width: '10',
height: '30',
},
};
test.describe.configure( { mode: 'serial' } );
@ -41,187 +53,428 @@ test.describe( 'General tab', { tag: '@gutenberg' }, () => {
let productId;
test.skip(
// skip(condition, description) can be used at runtime to skip a test
// eslint-disable-next-line jest/valid-title
isTrackingSupposedToBeEnabled,
'The block product editor is not being tested'
);
test( 'can create a simple product', async ( { page } ) => {
await page.goto( NEW_EDITOR_ADD_PRODUCT_URL );
await clickOnTab( 'General', page );
await page
.getByPlaceholder( 'e.g. 12 oz Coffee Mug' )
.fill( productData.name );
await test.step( 'add new product', async () => {
await page.goto( NEW_EDITOR_ADD_PRODUCT_URL );
} );
await page
.locator(
await test.step( 'add product name', async () => {
await clickOnTab( 'General', page );
await page
.getByPlaceholder( 'e.g. 12 oz Coffee Mug' )
// Have to use pressSequentially in order for the SKU to be auto-updated
// before we move to the SKU field and attempt to fill it in; otherwise,
// the SKU field can sometimes end up getting auto-updated after we have filled it in,
// wiping out the value we entered.
.pressSequentially( productData.name );
} );
await test.step( 'add simple product description', async () => {
const descriptionSimpleParagraph = page.locator(
'[data-template-block-id="product-description__content"] > p'
)
.fill( productData.descriptionSimple );
);
await page.getByText( 'Full editor' ).click();
await descriptionSimpleParagraph.fill(
productData.descriptionSimple
);
} );
const wordPressVersion = await getInstalledWordPressVersion();
await insertBlock( page, 'Heading', wordPressVersion );
await test.step( 'add full product description', async () => {
// Helps to ensure that block toolbar appears, by letting the editor
// know that the user is done typing.
await page.keyboard.press( 'Escape' );
const editorCanvasLocator = page.frameLocator(
'iframe[name="editor-canvas"]'
);
await page.getByText( 'Full editor' ).click();
await editorCanvasLocator
.locator( '[data-title="Heading"]' )
.fill( productData.descriptionTitle );
const wordPressVersion = await getInstalledWordPressVersion();
await insertBlock( page, 'Heading', wordPressVersion );
await editorCanvasLocator
.locator( '[data-title="Heading"]' )
.blur();
const editorCanvasLocator = page.frameLocator(
'iframe[name="editor-canvas"]'
);
await insertBlock( page, 'Paragraph', wordPressVersion );
await editorCanvasLocator
.locator( '[data-title="Heading"]' )
.fill( productData.descriptionTitle );
await editorCanvasLocator
.locator( '[data-title="Paragraph"]' )
.last()
.fill( productData.descriptionParagraph );
await editorCanvasLocator
.locator( '[data-title="Heading"]' )
.blur();
await page.getByRole( 'button', { name: 'Done' } ).click();
await insertBlock( page, 'Paragraph', wordPressVersion );
const previewContainerIframe = page
.locator( '.block-editor-block-preview__container' )
.frameLocator( 'iframe[title="Editor canvas"]' );
await editorCanvasLocator
.locator( '[data-title="Paragraph"]' )
.last()
.fill( productData.descriptionParagraph );
const descriptionTitle = previewContainerIframe.locator(
'[data-title="Heading"]'
);
const descriptionInitialParagraph = previewContainerIframe
.locator( '[data-title="Paragraph"]' )
.first();
const descriptionSecondParagraph = previewContainerIframe
.locator( '[data-title="Paragraph"]' )
.last();
await page.getByRole( 'button', { name: 'Done' } ).click();
} );
await expect( descriptionTitle ).toHaveText(
productData.descriptionTitle
);
await expect( descriptionInitialParagraph ).toHaveText(
productData.descriptionSimple
);
await expect( descriptionSecondParagraph ).toHaveText(
productData.descriptionParagraph
);
await test.step( 'verify full product description', async () => {
const previewContainerIframe = page
.locator( '.block-editor-block-preview__container' )
.frameLocator( 'iframe[title="Editor canvas"]' );
await descriptionTitle.click();
const descriptionTitle = previewContainerIframe.locator(
'[data-title="Heading"]'
);
const descriptionInitialParagraph = previewContainerIframe
.locator( '[data-title="Paragraph"]' )
.first();
const descriptionSecondParagraph = previewContainerIframe
.locator( '[data-title="Paragraph"]' )
.last();
await expect(
page.getByText( 'Edit in full editor' )
).toBeVisible();
await expect( descriptionTitle ).toHaveText(
productData.descriptionTitle
);
await expect( descriptionInitialParagraph ).toHaveText(
productData.descriptionSimple
);
await expect( descriptionSecondParagraph ).toHaveText(
productData.descriptionParagraph
);
await page
.locator(
'[data-template-block-id="basic-details"] .components-summary-control'
)
.last()
.fill( productData.summary );
await descriptionTitle.click();
// We blur the summary field to hide the toolbar before clicking on the regular price field.
await page
.locator(
'[data-template-block-id="basic-details"] .components-summary-control'
)
.last()
.blur();
await expect(
page.getByText( 'Edit in full editor' )
).toBeVisible();
} );
const regularPrice = page
.locator( 'input[name="regular_price"]' )
.first();
await regularPrice.waitFor( { state: 'visible' } );
await regularPrice.click();
await regularPrice.fill( productData.productPrice );
await test.step( 'add product summary', async () => {
await page
.locator(
'[data-template-block-id="basic-details"] .components-summary-control'
)
.last()
.fill( productData.summary );
const salePrice = page
.locator( 'input[name="sale_price"]' )
.first();
await salePrice.waitFor( { state: 'visible' } );
await salePrice.click();
await salePrice.fill( productData.salePrice );
// Blur the summary field to hide the toolbar before clicking on the regular price field.
await page
.locator(
'[data-template-block-id="basic-details"] .components-summary-control'
)
.last()
.blur();
} );
await page
.locator( '.woocommerce-product-header__actions' )
.getByRole( 'button', {
name: 'Publish',
} )
.click();
await test.step( 'add product price', async () => {
const regularPrice = page
.locator( 'input[name="regular_price"]' )
.first();
await regularPrice.waitFor( { state: 'visible' } );
await regularPrice.click();
await regularPrice.fill( productData.productPrice );
await expect(
page.getByLabel( 'Dismiss this notice' )
).toContainText( 'Product published' );
const salePrice = page
.locator( 'input[name="sale_price"]' )
.first();
await salePrice.waitFor( { state: 'visible' } );
await salePrice.click();
await salePrice.fill( productData.salePrice );
} );
const title = page.locator( '.woocommerce-product-header__title' );
await test.step( 'add custom fields', async () => {
await clickOnTab( 'Organization', page );
// Save product ID
const productIdRegex = /product%2F(\d+)/;
const url = page.url();
const productIdMatch = productIdRegex.exec( url );
productId = productIdMatch ? productIdMatch[ 1 ] : null;
const customFieldsAddNewButton = page
.getByLabel( 'Block: Product custom fields toggle control' )
.getByRole( 'button', { name: 'Add new' } );
await expect( productId ).toBeDefined();
await expect( title ).toHaveText( productData.name );
// When re-running the test without resetting the env,
// the custom fields toggle might be already checked,
// so we need to check if the "Add new" button is already visible.
//
// eslint-disable-next-line playwright/no-conditional-in-test
if ( ! ( await customFieldsAddNewButton.isVisible() ) ) {
// Toggle the "Show custom fields" so that the "Add new" button is visible
const customFieldsToggle = page.getByRole( 'checkbox', {
name: 'Show custom fields',
} );
await customFieldsToggle.scrollIntoViewIfNeeded();
// click() is used instead of check() because
// Playwright sometimes has issues with custom checkboxes:
// - https://github.com/microsoft/playwright/issues/13470
// - https://github.com/microsoft/playwright/issues/20893
// - https://github.com/microsoft/playwright/issues/27016
//
// eslint-disable-next-line playwright/no-conditional-in-test
if ( ! ( await customFieldsToggle.isChecked() ) ) {
await customFieldsToggle.click();
}
await customFieldsToggle.isEnabled();
}
await expect( customFieldsAddNewButton ).toBeVisible();
await customFieldsAddNewButton.click();
// Add custom fields modal
const modal = page.locator(
'.woocommerce-product-custom-fields__create-modal'
);
await expect(
modal.getByText( 'Add custom fields' )
).toBeVisible();
const nameInput = modal.getByLabel( 'Name' );
// Have to use pressSequentially in order to get the dropdown to show up and be able to select the option
await nameInput.pressSequentially(
productData.customFields[ 0 ].name
);
await expect(
modal.getByRole(
'option',
productData.customFields[ 0 ].name
)
).toBeVisible();
await nameInput.press( 'Enter' );
const valueInput = modal.getByLabel( 'Value' );
await valueInput.fill( productData.customFields[ 0 ].value );
await modal
.getByRole( 'button', { name: 'Add', exact: true } )
.click();
await expect(
modal.getByText( 'Add custom fields' )
).toBeHidden();
await expect(
page.getByText( productData.customFields[ 0 ].name )
).toBeVisible();
await expect(
page.getByText( productData.customFields[ 0 ].value )
).toBeVisible();
} );
await test.step( 'add inventory details', async () => {
await clickOnTab( 'Inventory', page );
await page
.getByLabel( 'SKU (Stock Keeping Unit)' )
.fill( productData.sku );
await page
.getByLabel( 'GTIN, UPC, EAN, or ISBN' )
.fill( productData.gtin );
} );
await test.step( 'add shipping details', async () => {
await clickOnTab( 'Shipping', page );
// Shipping class
await page
.getByLabel( 'Shipping class', { exact: true } )
//.locator( 'select[name="shipping_class"]' )
.selectOption( 'Add new shipping class' );
// New shipping class modal
const modal = page.locator(
'.woocommerce-add-new-shipping-class-modal'
);
await expect(
modal.getByText( 'New shipping class' )
).toBeVisible();
await modal
.getByLabel( 'Name (Required)' )
.fill( productData.shipping.shippingClassName );
await modal.getByText( 'Add' ).click();
await expect(
modal.getByText( 'New shipping class' )
).toBeHidden();
await expect(
page.getByLabel( 'Shipping class', { exact: true } )
).toHaveValue( productData.shipping.shippingClassName );
// Shipping dimensions
await page
.getByLabel( 'Width A' )
.fill( productData.shipping.width );
await page
.getByLabel( 'Length B' )
.fill( productData.shipping.length );
await page
.getByLabel( 'Height C' )
.fill( productData.shipping.height );
await page
.getByLabel( 'Weight' )
.fill( productData.shipping.weight );
} );
await test.step( 'publish the product', async () => {
await page
.locator( '.woocommerce-product-header__actions' )
.getByRole( 'button', {
name: 'Publish',
} )
.click();
await expect(
page.getByLabel( 'Dismiss this notice' )
).toContainText( 'Product published' );
const title = page.locator(
'.woocommerce-product-header__title'
);
// Save product ID
const productIdRegex = /product%2F(\d+)/;
const url = page.url();
const productIdMatch = productIdRegex.exec( url );
// This isn't really a conditional branch in the test;
// just making sure we don't blow up if the regex doesn't match
// (it will be caught in the expect below).
// eslint-disable-next-line playwright/no-conditional-in-test
productId = productIdMatch ? productIdMatch[ 1 ] : null;
expect( productId ).toBeDefined();
await expect( title ).toHaveText( productData.name );
} );
// Note for future refactoring: It would be good to reuse the verification step
// from product-create-simple.spec.js, as both tests are just verifying that the
// product was created correctly by looking at the front end.
await test.step( 'verify the saved product in frontend', async () => {
const permalink = await page
.locator( '.product-details-section__product-link a' )
.getAttribute( 'href' );
await page.goto( permalink );
// Verify product name
await expect(
page.getByRole( 'heading', {
name: productData.name,
} )
).toBeVisible();
// Verify price
await expect(
page.getByText( productData.productPrice ).first()
).toBeVisible();
await expect(
page.getByText( productData.salePrice ).first()
).toBeVisible();
// Verify summary
await expect(
page.getByText( productData.summary )
).toBeVisible();
// Verify description
await page.getByRole( 'tab', { name: 'Description' } ).click();
await expect(
page.getByText( productData.descriptionTitle )
).toBeVisible();
await expect(
page.getByText( productData.descriptionSimple )
).toBeVisible();
await expect(
page.getByText( productData.descriptionParagraph )
).toBeVisible();
// Verify inventory details
await expect(
page.getByText( `SKU: ${ productData.sku }` )
).toBeVisible();
// Note: GTIN is not displayed in the front end in the theme used in the test
// Note: Shipping class is not displayed in the front end in the theme used in the test
// Verify shipping dimensions
await page
.getByRole( 'tab', { name: 'Additional information' } )
.click();
await expect(
page.getByText( `Weight ${ productData.shipping.weight }` )
).toBeVisible();
await expect(
page.getByText(
`Dimensions ${ productData.shipping.length } × ${ productData.shipping.width } × ${ productData.shipping.height }`
)
).toBeVisible();
} );
} );
test( 'can not create a product with duplicated SKU', async ( {
page,
} ) => {
await page.goto( NEW_EDITOR_ADD_PRODUCT_URL );
await clickOnTab( 'General', page );
await page
.locator( '//input[@placeholder="e.g. 12 oz Coffee Mug"]' )
.fill( productData.name );
await page
.locator(
'[data-template-block-id="basic-details"] .components-summary-control'
)
.fill( productData.summary );
await test.step( 'add new product', async () => {
await page.goto( NEW_EDITOR_ADD_PRODUCT_URL );
} );
await page
.locator(
'[id^="wp-block-woocommerce-product-regular-price-field"]'
)
.first()
.fill( productData.productPrice );
await page
.locator( '.woocommerce-product-header__actions' )
.getByRole( 'button', {
name: 'Publish',
} )
.click();
await test.step( 'add product name', async () => {
await clickOnTab( 'General', page );
await page
.locator( '//input[@placeholder="e.g. 12 oz Coffee Mug"]' )
// Have to use pressSequentially in order for the SKU to be auto-updated
// before we move to the SKU field and attempt to fill it in; otherwise,
// the SKU field can sometimes end up getting auto-updated after we have filled it in,
// wiping out the value we entered.
.pressSequentially( productData.name );
} );
await expect(
page.locator( '.components-snackbar__content' )
).toContainText( 'Invalid or duplicated SKU.' );
await test.step( 'add product price', async () => {
const regularPrice = page
.locator( 'input[name="regular_price"]' )
.first();
await regularPrice.waitFor( { state: 'visible' } );
await regularPrice.click();
await regularPrice.fill( productData.productPrice );
} );
await test.step( 'add inventory details', async () => {
await clickOnTab( 'Inventory', page );
await page
.getByLabel( 'SKU (Stock Keeping Unit)' )
.fill( productData.sku );
} );
await test.step( 'publish the product', async () => {
await page
.locator( '.woocommerce-product-header__actions' )
.getByRole( 'button', {
name: 'Publish',
} )
.click();
await expect(
page.locator( '.components-snackbar__content' )
).toContainText( 'Invalid or duplicated SKU.' );
} );
} );
// Note for future refactoring: It would be good to reuse the verification step
// from product-create-simple.spec.js, as both tests are just verifying that the
// product that was created can be added to the cart in the front end.
test( 'can a shopper add the simple product to the cart', async ( {
page,
} ) => {
await page.goto( `/?post_type=product&p=${ productId }` );
await expect(
page.getByRole( 'heading', { name: productData.name } )
).toBeVisible();
await expect
.soft(
await page
.locator( 'del' )
.getByText( `$${ productData.productPrice }` )
.count()
)
.toBeGreaterThan( 0 );
await expect
.soft(
await page
.locator( 'ins' )
.getByText( `$${ productData.salePrice }` )
.count()
)
.toBeGreaterThan( 0 );
await page.locator( 'button[name="add-to-cart"]' ).click();
await page.getByRole( 'link', { name: 'View cart' } ).click();

View File

@ -113,7 +113,10 @@ test(
.getByRole( 'heading', { name: 'Attributes' } )
.isVisible();
await page.getByRole( 'button', { name: 'Add new' } ).click();
await page
// Using a selector because there are many "Add new" buttons on the page
.locator( '.woocommerce-add-attribute-list-item__add-button' )
.click();
await page
.getByRole( 'heading', { name: 'Add variation options' } )