diff --git a/cypress/apiRequests/Collections.js b/cypress/apiRequests/Collections.js index d761a892d..312eb4efe 100644 --- a/cypress/apiRequests/Collections.js +++ b/cypress/apiRequests/Collections.js @@ -1,3 +1,23 @@ +export function createCollection(name, slug = name) { + const mutation = `mutation { + collectionCreate(input:{ + name:"${name}", + slug:"${name}" + }){ + collectionErrors{ + field + message + } + collection{ + name + id + } + } + }`; + return cy + .sendRequestWithQuery(mutation) + .its("body.data.collectionCreate.collection"); +} export function getCollections(search) { const filter = search ? `, filter:{ diff --git a/cypress/apiRequests/Product.js b/cypress/apiRequests/Product.js index 153edfc1b..a2864d4fb 100644 --- a/cypress/apiRequests/Product.js +++ b/cypress/apiRequests/Product.js @@ -1,3 +1,4 @@ +import { stringify } from "../support/format/formatJson"; import { getValueWithDefault } from "./utils/Utils"; export function getFirstProducts(first, search) { @@ -22,6 +23,21 @@ export function getFirstProducts(first, search) { .sendRequestWithQuery(query) .then(resp => resp.body.data.products.edges); } +export function updateProduct(productId, input) { + const mutation = `mutation { + productUpdate(id:"${productId}", input:${stringify(input)} ){ + productErrors{ + field + message + } + product{ + id + } + } + } + `; + return cy.sendRequestWithQuery(mutation); +} export function updateChannelInProduct({ productId, diff --git a/cypress/elements/catalog/products/products-list.js b/cypress/elements/catalog/products/products-list.js index a9e1cf26a..4a9d110e8 100644 --- a/cypress/elements/catalog/products/products-list.js +++ b/cypress/elements/catalog/products/products-list.js @@ -1,6 +1,19 @@ export const PRODUCTS_LIST = { productsList: "[data-test-id][data-test='id']", + productsNames: "[data-test='name']", createProductBtn: "[data-test='add-product']", searchProducts: "[placeholder='Search Products...']", - emptyProductRow: "[class*='Skeleton']" + emptyProductRow: "[data-test-id='skeleton']", + showFiltersButton: '[data-test-id="show-filters-button"]', + filters: { + filterOption: '[data-test-id="filterOption"]', + productsOutOfStockOption: '[data-test-id="OUT_OF_STOCK"]', + filterBy: { + category: '[data-test-id="categories"]', + collection: '[data-test-id="collections"]', + productType: '[data-test-id="productType"]', + stock: '[data-test-id="stock"]' + }, + filterBySearchInput: '[data-test*="filterField"][data-test*="Input"]' + } }; diff --git a/cypress/elements/shared/sharedElements.js b/cypress/elements/shared/sharedElements.js index 17b4858b0..25291ab24 100644 --- a/cypress/elements/shared/sharedElements.js +++ b/cypress/elements/shared/sharedElements.js @@ -1,3 +1,4 @@ export const SHARED_ELEMENTS = { - header: "[data-test-id='page-header']" + header: "[data-test-id='page-header']", + progressBar: '[role="progressbar"]' }; diff --git a/cypress/integration/products/productsList/filteringProducts.js b/cypress/integration/products/productsList/filteringProducts.js new file mode 100644 index 000000000..dc2083b4b --- /dev/null +++ b/cypress/integration/products/productsList/filteringProducts.js @@ -0,0 +1,113 @@ +import faker from "faker"; + +import { createCollection } from "../../../apiRequests/Collections"; +import { PRODUCTS_LIST } from "../../../elements/catalog/products/products-list"; +import { + selectFilterOption, + selectProductsOutOfStock +} from "../../../steps/catalog/products/productsListSteps"; +import { urlList } from "../../../url/urlList"; +import { getDefaultChannel } from "../../../utils/channelsUtils"; +import { + createProductInChannel, + createTypeAttributeAndCategoryForProduct, + deleteProductsStartsWith, + updateProduct +} from "../../../utils/products/productsUtils"; +import { + createShipping, + deleteShippingStartsWith +} from "../../../utils/shippingUtils"; + +describe("Products", () => { + const startsWith = "Cy-"; + const name = `${startsWith}${faker.random.number()}`; + const stockQuantity = 747; + const price = 342; + let attribute; + let productType; + let category; + let warehouse; + let channel; + let collection; + + before(() => { + cy.clearSessionData().loginUserViaRequest(); + deleteShippingStartsWith(startsWith); + deleteProductsStartsWith(startsWith); + createTypeAttributeAndCategoryForProduct(name).then( + ({ + attribute: attributeResp, + productType: productTypeResp, + category: categoryResp + }) => { + attribute = attributeResp; + productType = productTypeResp; + category = categoryResp; + } + ); + createCollection(name).then( + collectionResp => (collection = collectionResp) + ); + getDefaultChannel() + .then(channelResp => { + channel = channelResp; + cy.fixture("addresses"); + }) + .then(addresses => { + createShipping({ + channelId: channel.id, + name, + address: addresses.plAddress + }); + }) + .then(({ warehouse: warehouseResp }) => { + warehouse = warehouseResp; + createProductInChannel({ + name, + channelId: channel.id, + warehouseId: warehouse.id, + quantityInWarehouse: stockQuantity, + price, + attributeId: attribute.id, + categoryId: category.id, + productTypeId: productType.id + }); + }) + .then(({ product: product }) => { + updateProduct(product.id, { collections: [collection.id] }); + }); + }); + beforeEach(() => { + cy.clearSessionData() + .loginUserViaRequest() + .visit(urlList.products); + }); + const filterProductsBy = ["category", "collection", "productType"]; + filterProductsBy.forEach(filterBy => { + it(`should filter products by ${filterBy}`, () => { + selectFilterOption(filterBy, name); + cy.getTextFromElement(PRODUCTS_LIST.productsNames).then(product => { + expect(product).to.includes(name); + }); + }); + }); + + it("should filter products out of stock", () => { + const productOutOfStock = `${startsWith}${faker.random.number()}`; + createProductInChannel({ + name: productOutOfStock, + channelId: channel.id, + warehouseId: warehouse.id, + quantityInWarehouse: 0, + productTypeId: productType.id, + attributeId: attribute.id, + categoryId: category.id, + price + }); + selectProductsOutOfStock(); + cy.getTextFromElement(PRODUCTS_LIST.productsNames).then(product => { + expect(product).to.includes(productOutOfStock); + }); + }); +}); diff --git a/cypress/steps/catalog/products/productsListSteps.js b/cypress/steps/catalog/products/productsListSteps.js new file mode 100644 index 000000000..3f83f12cf --- /dev/null +++ b/cypress/steps/catalog/products/productsListSteps.js @@ -0,0 +1,34 @@ +import { PRODUCTS_LIST } from "../../../elements/catalog/products/products-list"; +import { BUTTON_SELECTORS } from "../../../elements/shared/button-selectors"; +import { SHARED_ELEMENTS } from "../../../elements/shared/sharedElements"; + +export function selectFilterOption(filter, optionName) { + selectFilterBy(filter) + .get(PRODUCTS_LIST.filters.filterBySearchInput) + .type(optionName); + cy.contains(PRODUCTS_LIST.filters.filterOption, optionName) + .find(BUTTON_SELECTORS.checkbox) + .click(); + submitFilters(); +} +export function selectProductsOutOfStock() { + selectFilterBy("stock") + .get(PRODUCTS_LIST.filters.productsOutOfStockOption) + .click(); + submitFilters(); +} +function selectFilterBy(filter) { + return cy + .get(PRODUCTS_LIST.showFiltersButton) + .click() + .get(PRODUCTS_LIST.filters.filterBy[filter]) + .click(); +} +function submitFilters() { + cy.get(BUTTON_SELECTORS.submit) + .click() + .get(SHARED_ELEMENTS.progressBar) + .should("not.exist") + .get(PRODUCTS_LIST.emptyProductRow) + .should("not.exist"); +} diff --git a/cypress/support/format/formatJson.js b/cypress/support/format/formatJson.js new file mode 100644 index 000000000..6b9f30baf --- /dev/null +++ b/cypress/support/format/formatJson.js @@ -0,0 +1,12 @@ +export function stringify(obj_from_json) { + if (typeof obj_from_json !== "object" || Array.isArray(obj_from_json)) { + // not an object, stringify using native function + return JSON.stringify(obj_from_json); + } + // Implements recursive object serialization according to JSON spec + // but without quotes around the keys. + const props = Object.keys(obj_from_json) + .map(key => `${key}:${stringify(obj_from_json[key])}`) + .join(","); + return `{${props}}`; +} diff --git a/cypress/utils/products/productsUtils.js b/cypress/utils/products/productsUtils.js index 0864a89b6..0f6dc51e5 100644 --- a/cypress/utils/products/productsUtils.js +++ b/cypress/utils/products/productsUtils.js @@ -85,6 +85,11 @@ export function createProduct(attributeId, name, productTypeId, categoryId) { .createProduct(attributeId, name, productTypeId, categoryId) .its("body.data.productCreate.product"); } +export function updateProduct(productId, input) { + return productRequest + .updateProduct(productId, input) + .its("body.data.productUpdate.product"); +} export function createVariant({ productId, sku, diff --git a/src/components/Filter/Filter.tsx b/src/components/Filter/Filter.tsx index 694ca6e89..655a6cd22 100644 --- a/src/components/Filter/Filter.tsx +++ b/src/components/Filter/Filter.tsx @@ -111,6 +111,7 @@ const Filter: React.FC = props => { isFilterMenuOpened || isFilterActive })} onClick={() => setFilterMenuOpened(!isFilterMenuOpened)} + data-test-id="show-filters-button" > diff --git a/src/components/Filter/FilterAutocompleteField.tsx b/src/components/Filter/FilterAutocompleteField.tsx index b561d59c2..2868bc76b 100644 --- a/src/components/Filter/FilterAutocompleteField.tsx +++ b/src/components/Filter/FilterAutocompleteField.tsx @@ -131,7 +131,11 @@ const FilterAutocompleteField: React.FC = ({ )} {availableOptions.map(option => ( -
+
= props => { onClick={product && onRowClick(product.id)} className={classes.link} data-test="id" - data-test-id={product?.id} + data-test-id={product ? product?.id : "skeleton"} >