diff --git a/cypress/apiRequests/Category.js b/cypress/apiRequests/Category.js index ea02f4a18..cf94221fb 100644 --- a/cypress/apiRequests/Category.js +++ b/cypress/apiRequests/Category.js @@ -7,6 +7,7 @@ export function createCategory(name, slug = name) { } category{ id + name } } }`; diff --git a/cypress/apiRequests/Product.js b/cypress/apiRequests/Product.js index 2ccd8527f..f1b0ebc48 100644 --- a/cypress/apiRequests/Product.js +++ b/cypress/apiRequests/Product.js @@ -83,15 +83,34 @@ export function updateChannelPriceInVariant(variantId, channelId) { } `; return cy.sendRequestWithQuery(mutation); } -export function createProduct(attributeId, name, productType, category) { +export function createProduct( + attributeId, + name, + productType, + category, + collectionId, + description +) { + const collection = getValueWithDefault( + collectionId, + `collections:["${collectionId}"]` + ); + const descriptionLine = getValueWithDefault( + description, + `description:"{\\"blocks\\":[{\\"type\\":\\"paragraph\\",\\"data\\":{\\"text\\":\\"${description}\\"}}]}"` + ); const mutation = `mutation{ productCreate(input:{ attributes:[{ id:"${attributeId}" }] name:"${name}" + slug:"${name}" + seo:{title:"${name}" description:""} productType:"${productType}" category:"${category}" + ${collection} + ${descriptionLine} }){ product{ id @@ -177,6 +196,7 @@ export function createTypeProduct( } productType{ id + name } } } `; diff --git a/cypress/apiRequests/storeFront/ProductDetails.js b/cypress/apiRequests/storeFront/ProductDetails.js index 047a867e2..67450994f 100644 --- a/cypress/apiRequests/storeFront/ProductDetails.js +++ b/cypress/apiRequests/storeFront/ProductDetails.js @@ -1,7 +1,41 @@ -export function getProductDetails(productId, channelId) { +import { getValueWithDefault } from "../utils/Utils"; + +export function getProductDetails(productId, channelId, auth = "token") { + const privateMetadataLine = getValueWithDefault( + auth === "auth", + `privateMetadata{key value}` + ); const query = `fragment BasicProductFields on Product { id name + attributes{ + attribute{ + id + name + } + } + category{ + id + name + } + collections{ + id + name + } + description + seoTitle + slug + seoDescription + rating + metadata{ + key + value + } + ${privateMetadataLine} + productType{ + id + name + } } fragment Price on TaxedMoney { @@ -33,5 +67,5 @@ export function getProductDetails(productId, channelId) { availableForPurchase } }`; - return cy.sendRequestWithQuery(query, "token"); + return cy.sendRequestWithQuery(query, auth); } diff --git a/cypress/elements/catalog/products/product-details.js b/cypress/elements/catalog/products/product-details.js index 1a80f1515..7e685c086 100644 --- a/cypress/elements/catalog/products/product-details.js +++ b/cypress/elements/catalog/products/product-details.js @@ -13,5 +13,6 @@ export const PRODUCT_DETAILS = { descriptionInput: "[data-test-id='description']", ratingInput: "[name='rating']", skuInput: "[name='sku']", + collectionRemoveButtons: '[data-test-id="collectionRemove"]', variantRow: "[data-test-id='product-variant-row']" }; diff --git a/cypress/elements/shared/button-selectors.js b/cypress/elements/shared/button-selectors.js index 1a6fa43e6..84fa067d1 100644 --- a/cypress/elements/shared/button-selectors.js +++ b/cypress/elements/shared/button-selectors.js @@ -4,5 +4,6 @@ export const BUTTON_SELECTORS = { confirm: '[data-test="button-bar-confirm"]', goBackButton: "[data-test-id='app-header-back-button']", checkbox: "[type='checkbox']", - selectOption: "[data-test*='select-option']" + selectOption: "[data-test*='select-option']", + deleteButton: '[data-test="button-bar-delete"]' }; diff --git a/cypress/integration/collections.js b/cypress/integration/collections.js index c41de4a10..97cde6f50 100644 --- a/cypress/integration/collections.js +++ b/cypress/integration/collections.js @@ -21,7 +21,7 @@ import { isProductVisibleInSearchResult } from "../utils/storeFront/storeFrontPr describe("Collections", () => { const startsWith = "CyCollections-"; - const name = `${startsWith}${faker.random.number()}`; + const name = `${startsWith}${faker.datatype.number()}`; let attribute; let productType; @@ -35,6 +35,7 @@ describe("Collections", () => { productsUtils.deleteProductsStartsWith(startsWith); deleteCollectionsStartsWith(startsWith); deleteShippingStartsWith(startsWith); + channelsUtils.deleteChannelsStartsWith(startsWith); channelsUtils .getDefaultChannel() diff --git a/cypress/integration/products/createProduct.js b/cypress/integration/products/createProduct.js index 66d9dfb80..49bfa1831 100644 --- a/cypress/integration/products/createProduct.js +++ b/cypress/integration/products/createProduct.js @@ -9,7 +9,7 @@ import { fillUpPriceList, priceInputLists } from "../../steps/catalog/products/priceList"; -import { fillUpCommonFieldsForProductType } from "../../steps/catalog/products/productSteps"; +import { fillUpCommonFieldsForAllProductTypes } from "../../steps/catalog/products/productSteps"; import { selectChannelInDetailsPages } from "../../steps/channelsSteps"; import { urlList } from "../../url/urlList"; import { @@ -69,7 +69,7 @@ describe("Create product", () => { productOrganization: { productType: randomName }, attribute }; - fillUpCommonFieldsForProductType(productData).then( + fillUpCommonFieldsForAllProductTypes(productData).then( productOrgResp => (productData.productOrganization = productOrgResp) ); cy.addAliasToGraphRequest("ProductDetails"); @@ -96,7 +96,7 @@ describe("Create product", () => { productOrganization: { productType: randomName }, attribute }; - fillUpCommonFieldsForProductType(productData).then( + fillUpCommonFieldsForAllProductTypes(productData).then( productOrgResp => (productData.productOrganization = productOrgResp) ); selectChannelInDetailsPages(); diff --git a/cypress/integration/products/updatingProducts.js b/cypress/integration/products/updatingProducts.js new file mode 100644 index 000000000..360ed9207 --- /dev/null +++ b/cypress/integration/products/updatingProducts.js @@ -0,0 +1,142 @@ +import faker from "faker"; + +import { createCollection } from "../../apiRequests/Collections"; +import { getProductDetails } from "../../apiRequests/storeFront/ProductDetails"; +import { PRODUCT_DETAILS } from "../../elements/catalog/products/product-details"; +import { BUTTON_SELECTORS } from "../../elements/shared/button-selectors"; +import { metadataForms } from "../../steps/catalog/metadataSteps"; +import { fillUpCommonFieldsForAllProductTypes } from "../../steps/catalog/products/productSteps"; +import { productDetailsUrl } from "../../url/urlList"; +import { getDefaultChannel } from "../../utils/channelsUtils"; +import { deleteCollectionsStartsWith } from "../../utils/collectionsUtils"; +import { expectCorrectProductInformation } from "../../utils/products/checkProductInfo"; +import { + createCategory, + createProductInChannel, + createTypeAttributeAndCategoryForProduct, + deleteProductsStartsWith +} from "../../utils/products/productsUtils"; + +describe("Update products", () => { + const startsWith = "Cy-"; + const name = `${startsWith}${faker.random.number()}`; + const description = faker.lorem.sentences(2); + + let defaultChannel; + let collection; + let product; + let attribute; + + before(() => { + cy.clearSessionData().loginUserViaRequest(); + deleteProductsStartsWith(startsWith); + deleteCollectionsStartsWith(startsWith); + getDefaultChannel() + .then(channel => { + defaultChannel = channel; + createCollection(name); + }) + .then(collectionResp => { + collection = collectionResp; + createTypeAttributeAndCategoryForProduct(name); + }) + .then(({ attribute: attributeResp, category, productType }) => { + attribute = attributeResp; + createProductInChannel({ + attributeId: attribute.id, + categoryId: category.id, + productTypeId: productType.id, + channelId: defaultChannel.id, + name, + collectionId: collection.id, + description + }); + }) + .then(({ product: productResp }) => { + product = productResp; + }); + }); + it("Should update product", () => { + const updatedName = `${startsWith}${faker.random.number()}`; + let updatedCategory; + let updatedCollection; + createCategory(updatedName) + .then(categoryResp => { + updatedCategory = categoryResp; + createCollection(updatedName); + }) + .then(collectionResp => { + updatedCollection = collectionResp; + const productData = { + generalInfo: { + name: updatedName, + description: faker.lorem.sentence(), + rating: 3 + }, + seo: { + slug: updatedName, + title: "newTitle", + description: "New description." + }, + metadata: { + private: { + metadataForm: metadataForms.private, + name: "newPrivate", + value: "value1" + }, + public: { + metadataForm: metadataForms.public, + name: "newPublic", + value: "value2" + } + }, + productOrganization: { + category: updatedCategory.name, + collection: updatedCollection.name + } + }; + cy.visit(productDetailsUrl(product.id)); + cy.get(PRODUCT_DETAILS.collectionRemoveButtons).click(); + fillUpCommonFieldsForAllProductTypes(productData, false); + cy.addAliasToGraphRequest("UpdatePrivateMetadata"); + cy.addAliasToGraphRequest("UpdateMetadata"); + cy.addAliasToGraphRequest("ProductUpdate"); + cy.get(BUTTON_SELECTORS.confirm).click(); + cy.get(PRODUCT_DETAILS.confirmationMsg) + .should("be.visible") + .then(() => { + cy.wait("@ProductUpdate"); + cy.wait("@UpdateMetadata"); + cy.wait("@UpdatePrivateMetadata"); + productData.productOrganization.productType = name; + productData.attribute = attribute; + cy.loginUserViaRequest("token"); + }) + .then(() => { + getProductDetails(product.id, defaultChannel.slug, "auth").its( + "body.data.product" + ); + }) + .then(resp => { + expectCorrectProductInformation(resp, productData); + }); + }); + }); + it("should delete product", () => { + cy.visit(productDetailsUrl(product.id)); + cy.addAliasToGraphRequest("ProductDelete"); + cy.get(BUTTON_SELECTORS.deleteButton) + .click() + .get(BUTTON_SELECTORS.submit) + .click(); + cy.wait("@ProductDelete"); + cy.loginUserViaRequest("token") + .then(() => { + getProductDetails(product.id, defaultChannel.slug).its("body.data"); + }) + .then( + productResp => + expect(productResp.product, "Check if product exist").to.be.null + ); + }); +}); diff --git a/cypress/steps/catalog/metadataSteps.js b/cypress/steps/catalog/metadataSteps.js index 9f6f0ac46..24be76360 100644 --- a/cypress/steps/catalog/metadataSteps.js +++ b/cypress/steps/catalog/metadataSteps.js @@ -6,7 +6,8 @@ export const metadataForms = { }; export function addMetadataField({ metadataForm, name, value }) { - cy.get(metadataForm) + return cy + .get(metadataForm) .find(METADATA_FORM.addFieldButton) .click() .get(metadataForm) diff --git a/cypress/steps/catalog/products/productSteps.js b/cypress/steps/catalog/products/productSteps.js index 21ab3f758..e872cc097 100644 --- a/cypress/steps/catalog/products/productSteps.js +++ b/cypress/steps/catalog/products/productSteps.js @@ -40,28 +40,48 @@ function updateProductMenageInChannel(productUrl, menageSelector) { .click() .wait("@ProductChannelListingUpdate"); } -export function fillUpCommonFieldsForProductType({ +export function fillUpCommonFieldsForAllProductTypes( + { generalInfo, seo, metadata, productOrganization }, + createMode = true +) { + return fillUpAllCommonFieldsInCreateAndUpdate({ generalInfo, seo, metadata }) + .then(() => { + if (createMode) { + fillUpProductOrganization(productOrganization); + } else { + fillUpCollectionAndCategory({ + category: productOrganization.category, + collection: productOrganization.collection + }); + } + }) + .then(productOrgResp => productOrgResp); +} +export function fillUpAllCommonFieldsInCreateAndUpdate({ generalInfo, seo, - metadata, - productOrganization + metadata }) { - fillUpProductGeneralInfo(generalInfo); - editSeoSettings(seo); - addMetadataField(metadata.public); - addMetadataField(metadata.private); - return fillUpProductOrganization(productOrganization).then( - productOrgResp => productOrgResp - ); + return fillUpProductGeneralInfo(generalInfo) + .then(() => { + editSeoSettings(seo); + }) + .then(() => { + addMetadataField(metadata.public); + }) + .then(() => { + addMetadataField(metadata.private); + }); } export function fillUpProductGeneralInfo({ name, description, rating }) { - cy.get(PRODUCT_DETAILS.productNameInput) + return cy + .get(PRODUCT_DETAILS.productNameInput) .click() - .type(name) + .clearAndType(name) .get(PRODUCT_DETAILS.descriptionInput) - .type(description) + .clearAndType(description) .get(PRODUCT_DETAILS.ratingInput) - .type(rating); + .clearAndType(rating); } export function fillUpProductOrganization({ productType, @@ -72,8 +92,17 @@ export function fillUpProductOrganization({ return fillAutocompleteSelect(PRODUCT_DETAILS.productTypeInput, productType) .then(selected => { organization.productType = selected; - fillAutocompleteSelect(PRODUCT_DETAILS.categoryInput, category); + fillUpCollectionAndCategory({ category, collection }); }) + .then(collectionAndCategoryResp => { + organization.category = collectionAndCategoryResp.category; + organization.collection = collectionAndCategoryResp.collection; + return organization; + }); +} +export function fillUpCollectionAndCategory({ category, collection }) { + const organization = {}; + return fillAutocompleteSelect(PRODUCT_DETAILS.categoryInput, category) .then(selected => { organization.category = selected; fillAutocompleteSelect(PRODUCT_DETAILS.collectionInput, collection); diff --git a/cypress/steps/catalog/seoSteps.js b/cypress/steps/catalog/seoSteps.js index f29cf114a..22da400bc 100644 --- a/cypress/steps/catalog/seoSteps.js +++ b/cypress/steps/catalog/seoSteps.js @@ -3,9 +3,9 @@ export function editSeoSettings({ slug, title, description }) { cy.get(SEO_FORM.editSeoSettings) .click() .get(SEO_FORM.slugInput) - .type(slug) + .clearAndType(slug) .get(SEO_FORM.titleInput) - .type(title) + .clearAndType(title) .get(SEO_FORM.descriptionInput) - .type(description, { delay: 0 }); + .clearAndType(description, { delay: 0 }); } diff --git a/cypress/steps/shared/autocompleteSelect.js b/cypress/steps/shared/autocompleteSelect.js index 37385f8fa..05a168c83 100644 --- a/cypress/steps/shared/autocompleteSelect.js +++ b/cypress/steps/shared/autocompleteSelect.js @@ -6,7 +6,7 @@ export function fillAutocompleteSelect(selectSelector, option) { .get(BUTTON_SELECTORS.selectOption) .should("be.visible"); if (option) { - cy.get(selectSelector).type(option); + cy.get(selectSelector).clearAndType(option); cy.contains(BUTTON_SELECTORS.selectOption, option).click(); cy.wrap(option).as("option"); } else { diff --git a/cypress/support/elements/index.js b/cypress/support/elements/index.js index afb7b4475..136e4fd90 100644 --- a/cypress/support/elements/index.js +++ b/cypress/support/elements/index.js @@ -1,3 +1,9 @@ Cypress.Commands.add("getTextFromElement", element => cy.get(element).invoke("text") ); + +Cypress.Commands.add("clearAndType", { prevSubject: true }, (subject, text) => { + cy.wrap(subject) + .clear() + .type(text); +}); diff --git a/cypress/utils/products/checkProductInfo.js b/cypress/utils/products/checkProductInfo.js index fdc598b13..2cd06fe4e 100644 --- a/cypress/utils/products/checkProductInfo.js +++ b/cypress/utils/products/checkProductInfo.js @@ -27,37 +27,58 @@ export function expectCorrectProductVariantInformation( ); } function expectCorrectGeneralInformation(productResp, generalInfo) { - softExpect(productResp.name).to.be.eq(generalInfo.name); - softExpect(productResp.description).includes(generalInfo.description); - softExpect(productResp.rating).to.be.eq(generalInfo.rating); + softExpect(productResp.name, "Check product name").to.be.eq(generalInfo.name); + softExpect(productResp.description, "Check product description").includes( + generalInfo.description + ); + softExpect(productResp.rating, "Check product rate").to.be.eq( + generalInfo.rating + ); } function expectCorrectSeoInfo(productResp, seo) { - softExpect(productResp.slug).to.be.eq(seo.slug); - softExpect(productResp.seoTitle).to.be.eq(seo.title); - softExpect(productResp.seoDescription).to.be.eq(seo.description); + softExpect(productResp.slug, "Check product slug").to.be.eq(seo.slug); + softExpect(productResp.seoTitle, "Check product seo title").to.be.eq( + seo.title + ); + softExpect( + productResp.seoDescription, + "Check product seo description" + ).to.be.eq(seo.description); } function expectCorrectMetadataInfo(metadataResp, expectedMetadata) { softExpect( - expect(metadataResp).to.have.length(1), - softExpect(metadataResp[0].key).to.be.eq(expectedMetadata.name), - softExpect(metadataResp[0].value).to.be.eq(expectedMetadata.value) + expect(metadataResp, "Check metadata fields length").to.have.length(1), + softExpect(metadataResp[0].key, "Check product metadata key").to.be.eq( + expectedMetadata.name + ), + softExpect(metadataResp[0].value, "Check product metadata value").to.be.eq( + expectedMetadata.value + ) ); } function expectCorrectProductOrgInfo(productResp, productOrganization) { - softExpect(productResp.productType.name).to.be.eq( + softExpect(productResp.productType.name, "Check product type name").to.be.eq( productOrganization.productType ); - softExpect(productResp.category.name).to.be.eq(productOrganization.category); + softExpect(productResp.category.name, "Check category name").to.be.eq( + productOrganization.category + ); softExpect( - expect(productResp.collections).to.have.length(1), - softExpect(productResp.collections[0].name).to.be.eq( - productOrganization.collection - ) + expect( + productResp.collections, + "Check length of assigned collections" + ).to.have.length(1), + softExpect( + productResp.collections[0].name, + "Check collection name" + ).to.be.eq(productOrganization.collection) ); } function expectCorrectAttribute(attributes, attribute) { softExpect( expect(attributes).to.have.length(1), - softExpect(attributes[0].attribute.name).to.be.eq(attribute.name) + softExpect(attributes[0].attribute.name, "Check attribute name").to.be.eq( + attribute.name + ) ); } diff --git a/cypress/utils/products/productsUtils.js b/cypress/utils/products/productsUtils.js index 82a157016..6969d2e46 100644 --- a/cypress/utils/products/productsUtils.js +++ b/cypress/utils/products/productsUtils.js @@ -13,11 +13,20 @@ export function createProductInChannel({ price = 1, isPublished = true, isAvailableForPurchase = true, - visibleInListings = true + visibleInListings = true, + collectionId = null, + description = null }) { let product; let variants; - return createProduct(attributeId, name, productTypeId, categoryId) + return createProduct( + attributeId, + name, + productTypeId, + categoryId, + collectionId, + description + ) .then(productResp => { product = productResp; productRequest.updateChannelInProduct({ @@ -81,9 +90,23 @@ export function createCategory(name) { .createCategory(name) .its("body.data.categoryCreate.category"); } -export function createProduct(attributeId, name, productTypeId, categoryId) { +export function createProduct( + attributeId, + name, + productTypeId, + categoryId, + collectionId, + description +) { return productRequest - .createProduct(attributeId, name, productTypeId, categoryId) + .createProduct( + attributeId, + name, + productTypeId, + categoryId, + collectionId, + description + ) .its("body.data.productCreate.product"); } export function updateProduct(productId, input) { diff --git a/src/components/MultiAutocompleteSelectField/MultiAutocompleteSelectField.tsx b/src/components/MultiAutocompleteSelectField/MultiAutocompleteSelectField.tsx index f5930b3bc..a54b5bfd7 100644 --- a/src/components/MultiAutocompleteSelectField/MultiAutocompleteSelectField.tsx +++ b/src/components/MultiAutocompleteSelectField/MultiAutocompleteSelectField.tsx @@ -84,6 +84,7 @@ export interface MultiAutocompleteSelectFieldProps helperText?: string; label?: string; disabled?: boolean; + testId?: string; fetchChoices?: (value: string) => void; onChange: (event: React.ChangeEvent) => void; } @@ -107,6 +108,7 @@ const MultiAutocompleteSelectFieldComponent: React.FC handleSelect(value.value)} @@ -229,6 +232,7 @@ const MultiAutocompleteSelectFieldComponent: React.FC = ({ choices, fetchChoices, + testId, ...props }) => { const [query, setQuery] = React.useState(""); @@ -238,6 +242,7 @@ const MultiAutocompleteSelectField: React.FC {debounceFn => ( = props => { onChange={onCollectionChange} fetchChoices={fetchCollections} data-test="collections" + testId="collection" {...fetchMoreCollections} /> diff --git a/src/storybook/__snapshots__/Stories.test.ts.snap b/src/storybook/__snapshots__/Stories.test.ts.snap index 1367f592a..4f7023631 100644 --- a/src/storybook/__snapshots__/Stories.test.ts.snap +++ b/src/storybook/__snapshots__/Stories.test.ts.snap @@ -952,6 +952,7 @@ exports[`Storyshots Attributes / Attributes selected 1`] = `