diff --git a/plugins/woocommerce/changelog/e2e-split-variable-product-test b/plugins/woocommerce/changelog/e2e-split-variable-product-test new file mode 100644 index 00000000000..9c587cb0eaa --- /dev/null +++ b/plugins/woocommerce/changelog/e2e-split-variable-product-test @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Split can create product, attributes and variations, edit variations and delete variations into smaller tests to avoid timing out diff --git a/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-variable-product.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-variable-product.spec.js index ad4842cfb60..3e5d4f0c4e4 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-variable-product.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-variable-product.spec.js @@ -16,6 +16,9 @@ const defaultAttributes = [ 'val2', 'val1', 'val2' ]; const stockAmount = '100'; const lowStockAmount = '10'; +let fixedVariableProductId; +let fixedVariationIds; + async function deleteProductsAddedByTests( baseURL ) { const api = new wcApi( { url: baseURL, @@ -34,206 +37,678 @@ async function deleteProductsAddedByTests( baseURL ) { const ids = varProducts .map( ( { id } ) => id ) - .concat( manualProducts.map( ( { id } ) => id ) ); + .concat( manualProducts.map( ( { id } ) => id ) ) + .concat( [ fixedVariableProductId ] ); await api.post( 'products/batch', { delete: ids } ); } -async function resetVariableProductTour( baseURL, browser ) { - // Go to the product page, so that the `window.wp.data` module is available - const page = await browser.newPage( { baseURL: baseURL } ); - await page.goto( productPageURL ); +async function resetVariableProductTour( page ) { + await test.step( + 'Go to the product page, so that the `window.wp.data` module is available', + async () => { + await page.goto( productPageURL ); + } + ); - // Get the current user's ID and user preferences - const { id: userId, woocommerce_meta } = await page.evaluate( () => { - return window.wp.data.select( 'core' ).getCurrentUser(); + const { id: userId, woocommerce_meta } = await test.step( + "Get the current user's ID and user preferences", + async () => { + return await page.evaluate( () => { + return window.wp.data.select( 'core' ).getCurrentUser(); + } ); + } + ); + + const updatedWooCommerceMeta = await test.step( + 'Reset the variable product tour preference, so that it will be shown again', + async () => { + return { + ...woocommerce_meta, + variable_product_tour_shown: '', + }; + } + ); + + await test.step( 'Save the updated user preferences', async () => { + await page.evaluate( + async ( { userId, updatedWooCommerceMeta } ) => { + await window.wp.data.dispatch( 'core' ).saveUser( { + id: userId, + woocommerce_meta: updatedWooCommerceMeta, + } ); + }, + { userId, updatedWooCommerceMeta } + ); + } ); +} + +async function createVariableProductFixture( baseURL ) { + const api = new wcApi( { + url: baseURL, + consumerKey: process.env.CONSUMER_KEY, + consumerSecret: process.env.CONSUMER_SECRET, + version: 'wc/v3', } ); - // Reset the variable product tour preference, so that it will be shown again - const updatedWooCommerceMeta = { - ...woocommerce_meta, - variable_product_tour_shown: '', + const createVariableProductRequestPayload = { + name: 'Unbranded Granite Shirt', + type: 'variable', + attributes: [ + { + name: 'Colour', + visible: true, + variation: true, + options: [ 'Red', 'Green' ], + }, + { + name: 'Size', + visible: true, + variation: true, + options: [ 'Small', 'Medium' ], + }, + { + name: 'Logo', + visible: true, + variation: true, + options: [ 'Woo', 'WordPress' ], + }, + ], }; - // Save the updated user preferences - await page.evaluate( - async ( { userId, updatedWooCommerceMeta } ) => { - await window.wp.data.dispatch( 'core' ).saveUser( { - id: userId, - woocommerce_meta: updatedWooCommerceMeta, - } ); - }, - { userId, updatedWooCommerceMeta } + const batchCreateVariationsPayload = { + create: [ + { + attributes: [ + { name: 'Colour', option: 'Red' }, + { name: 'Size', option: 'Small' }, + { name: 'Logo', option: 'Woo' }, + ], + }, + { + attributes: [ + { name: 'Colour', option: 'Red' }, + { name: 'Size', option: 'Small' }, + { name: 'Logo', option: 'WordPress' }, + ], + }, + { + attributes: [ + { name: 'Colour', option: 'Red' }, + { name: 'Size', option: 'Medium' }, + { name: 'Logo', option: 'Woo' }, + ], + }, + { + attributes: [ + { name: 'Colour', option: 'Red' }, + { name: 'Size', option: 'Medium' }, + { name: 'Logo', option: 'WordPress' }, + ], + }, + { + attributes: [ + { name: 'Colour', option: 'Green' }, + { name: 'Size', option: 'Small' }, + { name: 'Logo', option: 'Woo' }, + ], + }, + { + attributes: [ + { name: 'Colour', option: 'Green' }, + { name: 'Size', option: 'Small' }, + { name: 'Logo', option: 'WordPress' }, + ], + }, + { + attributes: [ + { name: 'Colour', option: 'Green' }, + { name: 'Size', option: 'Medium' }, + { name: 'Logo', option: 'Woo' }, + ], + }, + { + attributes: [ + { name: 'Colour', option: 'Green' }, + { name: 'Size', option: 'Medium' }, + { name: 'Logo', option: 'WordPress' }, + ], + }, + ], + }; + + await test.step( + 'Create the variable product and its attributes.', + async () => { + await api + .post( 'products', createVariableProductRequestPayload ) + .then( ( response ) => { + fixedVariableProductId = response.data.id; + } ); + } ); + + await test.step( 'Generate all variations.', async () => { + await api + .post( + `products/${ fixedVariableProductId }/variations/batch`, + batchCreateVariationsPayload + ) + .then( ( response ) => { + fixedVariationIds = response.data.create + .map( ( variation ) => variation.id ) + .sort(); + } ); + } ); } test.describe( 'Add New Variable Product Page', () => { test.use( { storageState: process.env.ADMINSTATE } ); - test.afterAll( async ( { baseURL, browser } ) => { - await deleteProductsAddedByTests( baseURL ); - await resetVariableProductTour( baseURL, browser ); + test.beforeAll( async ( { baseURL } ) => { + await test.step( + 'Set up a variable product fixture through the REST API.', + async () => { + await createVariableProductFixture( baseURL ); + } + ); } ); - test( 'can create product, attributes and variations, edit variations and delete variations', async ( { + test.afterAll( async ( { baseURL } ) => { + await deleteProductsAddedByTests( baseURL ); + } ); + + test( 'can create product, attributes and variations', async ( { page, } ) => { - await page.goto( productPageURL ); - await page.fill( '#title', variableProductName ); - await page.selectOption( '#product-type', 'variable' ); + await test.step( 'Reset the variable product tour.', async () => { + await resetVariableProductTour( page ); + } ); - await page - .locator( '.attribute_tab' ) - .getByRole( 'link', { name: 'Attributes' } ) - .scrollIntoViewIfNeeded(); + await test.step( 'Go to "Products > Add new" page.', async () => { + await page.goto( productPageURL ); + } ); + + await test.step( + `Type "${ variableProductName }" into the "Product name" input field.`, + async () => { + await page.fill( '#title', variableProductName ); + } + ); + + await test.step( + 'Select the "Variable product" product type.', + async () => { + await page.selectOption( '#product-type', 'variable' ); + } + ); + + await test.step( + 'Scroll into the "Attributes" tab and click it.', + async () => { + const attributesTab = page + .locator( '.attribute_tab' ) + .getByRole( 'link', { name: 'Attributes' } ); + + await attributesTab.scrollIntoViewIfNeeded(); + + await attributesTab.click(); + } + ); // the tour only seems to display when not running headless, so just make sure - if ( await page.locator( '.woocommerce-tour-kit-step__heading' ).isVisible() ) { - // dismiss the variable product tour - await page - .getByRole( 'button', { name: 'Close Tour' } ) - .click(); - - // wait for the tour's dismissal to be saved - await page.waitForResponse( - ( response ) => - response.url().includes( '/users/' ) && - response.status() === 200 - ); - } - - await page.click( 'a[href="#product_attributes"]' ); - - // add 3 attributes - for ( let i = 0; i < 3; i++ ) { - if ( i > 0 ) { - await page.getByRole( 'button', { name: 'Add' } ) - .nth(2) - .click(); + const tourWasDisplayed = await test.step( + 'See if the tour was displayed.', + async () => { + return await page + .locator( '.woocommerce-tour-kit-step__heading' ) + .isVisible(); } - await page.waitForSelector( - `input[name="attribute_names[${ i }]"]` + ); + + if ( tourWasDisplayed ) { + await test.step( 'Tour was displayed, so dismiss it.', async () => { + await page + .getByRole( 'button', { name: 'Close Tour' } ) + .click(); + } ); + + await test.step( + "Wait for the tour's dismissal to be saved", + async () => { + await page.waitForResponse( + ( response ) => + response.url().includes( '/users/' ) && + response.status() === 200 + ); + } ); - - await page - .locator( `input[name="attribute_names[${ i }]"]` ) - .first() - .type( `attr #${ i + 1 }` ); - await page - .locator( `textarea[name="attribute_values[${ i }]"]` ) - .first() - .type( 'val1 | val2' ); } - await page.getByRole( 'button', { name: 'Save attributes'} ).click( { clickCount: 3 }); + await test.step( 'Add 3 attributes.', async () => { + for ( let i = 0; i < 3; i++ ) { + if ( i > 0 ) { + await test.step( "Click 'Add'.", async () => { + await page + .locator( '.add-attribute-container' ) + .getByRole( 'button', { name: 'Add' } ) + .click(); + } ); + } - // wait for the tour's dismissal to be saved - await page.waitForResponse( - ( response ) => - response.url().includes( '/post.php' ) && - response.status() === 200 - ); + await test.step( + `Add the attribute "attr #${ + i + 1 + }" with values "val1 | val2"`, + async () => { + await test.step( + 'Wait for the "Attribute name" input field to appear.', + async () => { + await page.waitForSelector( + `input[name="attribute_names[${ i }]"]` + ); + } + ); - // Save before going to the Variations tab to prevent variations from all attributes to be automatically created - await page.locator( '#save-post' ).click(); - await expect( - page.getByText( 'Product draft updated. ' ) - ).toBeVisible(); + await test.step( + `Type "attr #${ + i + 1 + }" in the "Attribute name" input field.`, + async () => { + await page + .locator( + `input[name="attribute_names[${ i }]"]` + ) + .first() + .type( `attr #${ i + 1 }` ); + } + ); - await page.click( 'a[href="#variable_product_options"]' ); + await test.step( + 'Type the attribute values "val1 | val2".', + async () => { + await page + .locator( + `textarea[name="attribute_values[${ i }]"]` + ) + .first() + .type( 'val1 | val2' ); + } + ); - // event listener for handling the link_all_variations confirmation dialog - page.on( 'dialog', ( dialog ) => dialog.accept() ); + await test.step( + 'Click "Save attributes".', + async () => { + await page + .getByRole( 'button', { + name: 'Save attributes', + } ) + .click( { clickCount: 3 } ); + } + ); - // generate variations from all attributes - await page.click( 'button.generate_variations' ); - - // verify variations have the correct attribute values - for ( let i = 0; i < 8; i++ ) { - const val1 = 'val1'; - const val2 = 'val2'; - const attr3 = !! ( i % 2 ); // 0-1,4-5 / 2-3,6-7 - const attr2 = i % 4 > 1; // 0-3 / 4-7 - const attr1 = i > 3; - await expect( - page.locator( `select[name="attribute_attr-1[${ i }]"]` ) - ).toHaveValue( attr1 ? val2 : val1 ); - await expect( - page.locator( `select[name="attribute_attr-2[${ i }]"]` ) - ).toHaveValue( attr2 ? val2 : val1 ); - await expect( - page.locator( `select[name="attribute_attr-3[${ i }]"]` ) - ).toHaveValue( attr3 ? val2 : val1 ); - } - - await page.locator( '#save-post' ).click(); - await expect( page.locator( '#message.notice-success' ) ).toContainText( - 'Product draft updated.' - ); - - // set variation attributes and bulk edit variations - await page.click( 'a[href="#variable_product_options"]' ); - - // set the variation attributes - await page.click( - '#variable_product_options .toolbar-top a.expand_all' - ); - await page.check( 'input[name="variable_is_virtual[0]"]' ); - await page.fill( - 'input[name="variable_regular_price[0]"]', - variationOnePrice - ); - await page.check( 'input[name="variable_is_virtual[1]"]' ); - await page.fill( - 'input[name="variable_regular_price[1]"]', - variationTwoPrice - ); - await page.check( 'input[name="variable_manage_stock[2]"]' ); - await page.fill( - 'input[name="variable_regular_price[2]"]', - variationThreePrice - ); - await page.fill( 'input[name="variable_weight[2]"]', productWeight ); - await page.fill( 'input[name="variable_length[2]"]', productLength ); - await page.fill( 'input[name="variable_width[2]"]', productWidth ); - await page.fill( 'input[name="variable_height[2]"]', productHeight ); - await page.keyboard.press( 'ArrowUp' ); - await page.click( 'button.save-variation-changes' ); - - // bulk-edit variations - await page.click( - '#variable_product_options .toolbar-top a.expand_all' - ); - for ( let i = 0; i < 4; i++ ) { - const checkBox = page.locator( - `input[name="variable_is_downloadable[${ i }]"]` - ); - await expect( checkBox ).not.toBeChecked(); - } - await page.selectOption( '#field_to_edit', 'toggle_downloadable', { - force: true, + await test.step( + "Wait for the tour's dismissal to be saved", + async () => { + await page.waitForResponse( + ( response ) => + response + .url() + .includes( '/post.php' ) && + response.status() === 200 + ); + } + ); + } + ); + } } ); - await page.click( - '#variable_product_options .toolbar-top a.expand_all' + + await test.step( + 'Save before going to the Variations tab to prevent variations from all attributes to be automatically created.', + async () => { + await page.locator( '#save-post' ).click(); + } ); - for ( let i = 0; i < 4; i++ ) { - const checkBox = await page.locator( - `input[name="variable_is_downloadable[${ i }]"]` - ); - await expect( checkBox ).toBeChecked(); - } - await page.locator( '#save-post' ).click(); + await test.step( + 'Expect the "Product draft updated." notice to appear.', + async () => { + await expect( + page.getByText( 'Product draft updated. ' ) + ).toBeVisible(); + } + ); - // delete all variations - await page.click( 'a[href="#variable_product_options"]' ); - await page.waitForLoadState( 'networkidle' ); - await page.selectOption( '#field_to_edit', 'delete_all' ); - await page.waitForSelector( '.woocommerce_variation', { - state: 'detached', + await test.step( 'Click on the "Variations" tab.', async () => { + await page.click( 'a[href="#variable_product_options"]' ); } ); - const variationsCount = await page.$$( '.woocommerce_variation' ); - await expect( variationsCount ).toHaveLength( 0 ); + + await test.step( + 'Click on the "Generate variations" button.', + async () => { + // event listener for handling the link_all_variations confirmation dialog + page.on( 'dialog', ( dialog ) => dialog.accept() ); + + await page.click( 'button.generate_variations' ); + } + ); + + await test.step( + 'Expect variations to have the correct attribute values', + async () => { + for ( let i = 0; i < 8; i++ ) { + const val1 = 'val1'; + const val2 = 'val2'; + const attr3 = !! ( i % 2 ); // 0-1,4-5 / 2-3,6-7 + const attr2 = i % 4 > 1; // 0-3 / 4-7 + const attr1 = i > 3; + await expect( + page.locator( + `select[name="attribute_attr-1[${ i }]"]` + ) + ).toHaveValue( attr1 ? val2 : val1 ); + await expect( + page.locator( + `select[name="attribute_attr-2[${ i }]"]` + ) + ).toHaveValue( attr2 ? val2 : val1 ); + await expect( + page.locator( + `select[name="attribute_attr-3[${ i }]"]` + ) + ).toHaveValue( attr3 ? val2 : val1 ); + } + } + ); + + await test.step( 'Click "Save Draft" button.', async () => { + await page.locator( '#save-post' ).click(); + } ); + + await test.step( + 'Expect the "Product draft updated." notice to appear.', + async () => { + await expect( + page.locator( '#message.notice-success' ) + ).toContainText( 'Product draft updated.' ); + } + ); + } ); + + test( 'can individually edit variations', async ( { page } ) => { + const variationRows = page.locator( '.woocommerce_variation' ); + const firstVariation = variationRows.filter( { + hasText: `#${ fixedVariationIds[ 0 ] }`, + } ); + const secondVariation = variationRows.filter( { + hasText: `#${ fixedVariationIds[ 1 ] }`, + } ); + const thirdVariation = variationRows.filter( { + hasText: `#${ fixedVariationIds[ 2 ] }`, + } ); + + await test.step( 'Go to the "Edit product" page.', async () => { + await page.goto( + `/wp-admin/post.php?post=${ fixedVariableProductId }&action=edit` + ); + } ); + + await test.step( 'Click on the "Variations" tab.', async () => { + await page.click( 'a[href="#variable_product_options"]' ); + } ); + + await test.step( 'Expand all variations.', async () => { + await page.click( + '#variable_product_options .toolbar-top a.expand_all' + ); + } ); + + await test.step( 'Edit the first variation.', async () => { + await test.step( 'Check the "Virtual" checkbox.', async () => { + await firstVariation + .getByRole( 'checkbox', { + name: 'Virtual', + } ) + .check(); + } ); + + await test.step( + `Set regular price to "${ variationOnePrice }".`, + async () => { + await firstVariation + .getByRole( 'textbox', { name: 'Regular price' } ) + .fill( variationOnePrice ); + } + ); + } ); + + await test.step( 'Edit the second variation.', async () => { + await test.step( 'Check the "Virtual" checkbox.', async () => { + await secondVariation + .getByRole( 'checkbox', { + name: 'Virtual', + } ) + .check(); + } ); + + await test.step( + `Set regular price to "${ variationTwoPrice }".`, + async () => { + await secondVariation + .getByRole( 'textbox', { name: 'Regular price' } ) + .fill( variationTwoPrice ); + } + ); + } ); + + await test.step( 'Edit the third variation.', async () => { + await test.step( 'Check "Manage stock?"', async () => { + await thirdVariation + .getByRole( 'checkbox', { name: 'Manage stock?' } ) + .check(); + } ); + + await test.step( + `Set regular price to "${ variationThreePrice }".`, + async () => { + await thirdVariation + .getByRole( 'textbox', { name: 'Regular price' } ) + .fill( variationThreePrice ); + } + ); + + await test.step( 'Set the weight and dimensions.', async () => { + await thirdVariation + .getByRole( 'textbox', { name: 'Weight' } ) + .type( productWeight ); + + await thirdVariation + .getByRole( 'textbox', { name: 'Length' } ) + .type( productLength ); + + await thirdVariation + .getByRole( 'textbox', { name: 'Width' } ) + .type( productWidth ); + + await thirdVariation + .getByRole( 'textbox', { name: 'Height' } ) + .type( productHeight ); + } ); + } ); + + await test.step( 'Click "Save changes".', async () => { + await page.click( 'button.save-variation-changes' ); + await page.waitForLoadState( 'networkidle' ); + } ); + + await test.step( 'Click on the "Variations" tab.', async () => { + await page.click( 'a[href="#variable_product_options"]' ); + } ); + + await test.step( 'Expand all variations.', async () => { + await page.click( + '#variable_product_options .toolbar-top a.expand_all' + ); + } ); + + await test.step( + 'Expect the first variation to be virtual.', + async () => { + await expect( + firstVariation.getByRole( 'checkbox', { + name: 'Virtual', + } ) + ).toBeChecked(); + } + ); + + await test.step( + `Expect the regular price of the first variation to be "${ variationOnePrice }".`, + async () => { + await expect( + firstVariation.getByRole( 'textbox', { + name: 'Regular price', + } ) + ).toHaveValue( variationOnePrice ); + } + ); + + await test.step( + 'Expect the second variation to be virtual.', + async () => { + await expect( + secondVariation.getByRole( 'checkbox', { + name: 'Virtual', + } ) + ).toBeChecked(); + } + ); + + await test.step( + `Expect the regular price of the second variation to be "${ variationTwoPrice }".`, + async () => { + await expect( + secondVariation.getByRole( 'textbox', { + name: 'Regular price', + } ) + ).toHaveValue( variationTwoPrice ); + } + ); + + await test.step( + 'Expect the "Manage stock?" checkbox of the third variation to be checked.', + async () => { + await expect( + thirdVariation.getByRole( 'checkbox', { + name: 'Manage stock?', + } ) + ).toBeChecked(); + } + ); + + await test.step( + `Expect the regular price of the third variation to be "${ variationThreePrice }".`, + async () => { + await expect( + thirdVariation.getByRole( 'textbox', { + name: 'Regular price', + } ) + ).toHaveValue( variationThreePrice ); + } + ); + + await test.step( + 'Expect the weight and dimensions of the third variation to be correct.', + async () => { + await expect( + thirdVariation.getByRole( 'textbox', { name: 'Weight' } ) + ).toHaveValue( productWeight ); + + await expect( + thirdVariation.getByRole( 'textbox', { name: 'Length' } ) + ).toHaveValue( productLength ); + + await expect( + thirdVariation.getByRole( 'textbox', { name: 'Width' } ) + ).toHaveValue( productWidth ); + + await expect( + thirdVariation.getByRole( 'textbox', { name: 'Height' } ) + ).toHaveValue( productHeight ); + } + ); + } ); + + test( 'can bulk edit variations', async ( { page } ) => { + await test.step( 'Go to the "Edit product" page.', async () => { + await page.goto( + `/wp-admin/post.php?post=${ fixedVariableProductId }&action=edit` + ); + } ); + + await test.step( 'Click on the "Variations" tab.', async () => { + await page.click( 'a[href="#variable_product_options"]' ); + } ); + + await test.step( + 'Select the \'Toggle "Downloadable"\' bulk action.', + async () => { + await page.selectOption( + '#field_to_edit', + 'toggle_downloadable' + ); + } + ); + + await test.step( 'Expand all variations.', async () => { + await page.click( + '#variable_product_options .toolbar-top a.expand_all' + ); + } ); + + await test.step( + 'Expect all "Downloadable" checkboxes to be checked.', + async () => { + const checkBoxes = page.locator( + 'input[name^="variable_is_downloadable"]' + ); + const count = await checkBoxes.count(); + + for ( let i = 0; i < count; i++ ) { + await expect( checkBoxes.nth( i ) ).toBeChecked(); + } + } + ); + } ); + + test( 'can delete all variations', async ( { page } ) => { + await test.step( 'Go to the "Edit product" page.', async () => { + await page.goto( + `/wp-admin/post.php?post=${ fixedVariableProductId }&action=edit` + ); + } ); + + await test.step( 'Click on the "Variations" tab.', async () => { + await page.click( 'a[href="#variable_product_options"]' ); + } ); + + await test.step( + 'Select the bulk action "Delete all variations".', + async () => { + page.on( 'dialog', ( dialog ) => dialog.accept() ); + await page.selectOption( '#field_to_edit', 'delete_all' ); + } + ); + + await test.step( + 'Expect that there are no more variations.', + async () => { + await expect( + page.locator( '.woocommerce_variation' ) + ).toHaveCount( 0 ); + } + ); } ); test( 'can manually add a variation, manage stock levels, set variation defaults and remove a variation', async ( { @@ -249,11 +724,13 @@ test.describe( 'Add New Variable Product Page', () => { .scrollIntoViewIfNeeded(); // the tour only seems to display when not running headless, so just make sure - if ( await page.locator( '.woocommerce-tour-kit-step__heading' ).isVisible() ) { - // dismiss the variable product tour + if ( await page - .getByRole( 'button', { name: 'Close Tour' } ) - .click(); + .locator( '.woocommerce-tour-kit-step__heading' ) + .isVisible() + ) { + // dismiss the variable product tour + await page.getByRole( 'button', { name: 'Close Tour' } ).click(); // wait for the tour's dismissal to be saved await page.waitForResponse(