diff --git a/CHANGELOG.md b/CHANGELOG.md index 88c8d972c..c33a975df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,8 @@ All notable, unreleased changes to this project will be documented in this file. - Drop descriptionJson and contentJson fields - #950 by @jwm0 - Add error tracking with Sentry adapter - #956 by @jwm0 - Add OAuth2 login with OpenID support - #963 by @orzechdev +- Fix no channels crash - #984 by @dominik-zeglen +- Update webhooks - #982 by @piotrgrundas # 2.11.1 diff --git a/cypress/apiRequests/Attribute.js b/cypress/apiRequests/Attribute.js index 75b13c08d..f1c180d01 100644 --- a/cypress/apiRequests/Attribute.js +++ b/cypress/apiRequests/Attribute.js @@ -1,48 +1,50 @@ class Attribute { createAttribute(name) { const mutation = `mutation{ - attributeCreate(input:{ - name:"${name}" - valueRequired:false - type:PRODUCT_TYPE - }){ - attribute{ - id - } - attributeErrors{ - field - message - } - } - }`; + attributeCreate(input:{ + name:"${name}" + valueRequired:false + type:PRODUCT_TYPE + }){ + attribute{ + id + } + attributeErrors{ + field + message + } + } + }`; return cy.sendRequestWithQuery(mutation); } getAttributes(first, search) { const mutation = `query{ - attributes(first:${first}, filter:{ - search:"${search}" - }){ - edges{ - node{ - id - name - } - } + attributes(first:${first}, filter:{ + search:"${search}" + }){ + edges{ + node{ + id + name } - }`; - return cy.sendRequestWithQuery(mutation); + } + } + }`; + return cy + .sendRequestWithQuery(mutation) + .then(resp => resp.body.data.attributes.edges); } deleteAttribute(attributeId) { const mutation = `mutation{ - attributeDelete(id:"${attributeId}"){ - attributeErrors{ - field - message - } - } - }`; + attributeDelete(id:"${attributeId}"){ + attributeErrors{ + field + message + } + } + }`; return cy.sendRequestWithQuery(mutation); } } diff --git a/cypress/apiRequests/Category.js b/cypress/apiRequests/Category.js index a20d10a23..ad5d4f0dd 100644 --- a/cypress/apiRequests/Category.js +++ b/cypress/apiRequests/Category.js @@ -1,42 +1,44 @@ class Category { createCategory(name, slug = name) { const mutation = `mutation{ - categoryCreate(input:{name:"${name}", slug: "${slug}"}){ - productErrors{ - field - message - } - category{ - id - } + categoryCreate(input:{name:"${name}", slug: "${slug}"}){ + productErrors{ + field + message } - }`; + category{ + id + } + } + }`; return cy.sendRequestWithQuery(mutation); } getCategories(first, search) { const mutation = `query{ - categories(first:${first}, filter:{ - search:"${search}" - }){ - edges{ - node{ - id - name - } - } + categories(first:${first}, filter:{ + search:"${search}" + }){ + edges{ + node{ + id + name } - }`; - return cy.sendRequestWithQuery(mutation); - } - deleteCategory(categoryId) { - const mutation = `mutation{ - categoryDelete(id:"${categoryId}"){ - productErrors{ - field - message } } }`; + return cy + .sendRequestWithQuery(mutation) + .then(resp => resp.body.data.categories.edges); + } + deleteCategory(categoryId) { + const mutation = `mutation{ + categoryDelete(id:"${categoryId}"){ + productErrors{ + field + message + } + } + }`; return cy.sendRequestWithQuery(mutation); } } diff --git a/cypress/apiRequests/Channels.js b/cypress/apiRequests/Channels.js index 603d442ed..6d8872798 100644 --- a/cypress/apiRequests/Channels.js +++ b/cypress/apiRequests/Channels.js @@ -1,53 +1,24 @@ class Channels { createChannel(isActive, name, slug, currencyCode) { const createChannelMutation = `mutation{ - channelCreate(input: { - isActive: ${isActive} - name: "${name}" - slug: "${slug}" - currencyCode: "${currencyCode}" - }){ - channel{ - name - slug + channelCreate(input: { + isActive: ${isActive} + name: "${name}" + slug: "${slug}" + currencyCode: "${currencyCode}" + }){ + channel{ + id + name + slug + } + channelErrors{ + code + message + } } - channelErrors{ - code - message - } - } - }`; - cy.sendRequestWithQuery(createChannelMutation); - } - - deleteTestChannels(nameStartsWith) { - const getChannelsInfoQuery = `query{ - channels{ - name - id - isActive - slug - currencyCode - } - } - `; - cy.sendRequestWithQuery(getChannelsInfoQuery).then(resp => { - const channels = new Set(resp.body.data.channels); - channels.forEach(element => { - if (element.name.startsWith(nameStartsWith)) { - const targetChannels = Array.from(channels).filter(function(channel) { - return ( - element.currencyCode === channel.currencyCode && - element.id !== channel.id - ); - }); - if (targetChannels[0]) { - this.deleteChannel(element.id, targetChannels[0].id); - channels.delete(element); - } - } - }); - }); + }`; + return cy.sendRequestWithQuery(createChannelMutation); } getChannels() { const getChannelsInfoQuery = `query{ @@ -62,10 +33,11 @@ class Channels { `; return cy.sendRequestWithQuery(getChannelsInfoQuery); } - deleteChannel(channelId, targetChennelId) { + + deleteChannel(channelId, targetChannelId) { const deleteChannelMutation = `mutation{ channelDelete(id: "${channelId}", input:{ - targetChannel: "${targetChennelId}" + targetChannel: "${targetChannelId}" }){ channel{ name diff --git a/cypress/apiRequests/Checkout.js b/cypress/apiRequests/Checkout.js new file mode 100644 index 000000000..28f9fbf69 --- /dev/null +++ b/cypress/apiRequests/Checkout.js @@ -0,0 +1,69 @@ +class Checkout { + createCheckout(channelSlug, email, productQuantity, variantsList) { + const lines = variantsList.map( + variant => `{quantity:${productQuantity} + variantId:"${variant.id}"}` + ); + const mutation = `mutation{ + checkoutCreate(input:{ + channel:"${channelSlug}" + email:"${email}" + lines: [${lines.join()}] + }){ + checkoutErrors{ + field + message + } + created + checkout{ + id + } + } + }`; + return cy.sendRequestWithQuery(mutation); + } + addShippingMethod(checkoutId, shippingMethodId) { + const mutation = `mutation{ + checkoutShippingMethodUpdate(checkoutId:"${checkoutId}", + shippingMethodId:"${shippingMethodId}"){ + checkoutErrors{ + message + field + } + } + }`; + return cy.sendRequestWithQuery(mutation); + } + addPayment(checkoutId, gateway, token) { + const mutation = `mutation{ + checkoutPaymentCreate(checkoutId:"${checkoutId}", + input:{ + gateway: "${gateway}" + token:"${token}" + }){ + paymentErrors{ + field + message + } + } + }`; + return cy.sendRequestWithQuery(mutation); + } + completeCheckout(checkoutId) { + const mutation = `mutation{ + checkoutComplete(checkoutId:"${checkoutId}"){ + order{ + id + } + confirmationNeeded + confirmationData + checkoutErrors{ + field + message + } + } + }`; + return cy.sendRequestWithQuery(mutation); + } +} +export default Checkout; diff --git a/cypress/apiRequests/Customer.js b/cypress/apiRequests/Customer.js new file mode 100644 index 000000000..336dbf698 --- /dev/null +++ b/cypress/apiRequests/Customer.js @@ -0,0 +1,85 @@ +export class Customer { + createCustomer(email, customerName, address, isActive = false) { + const mutation = ` + mutation{ + customerCreate(input:{ + firstName: "${customerName}" + lastName: "${customerName}" + email: "${email}" + isActive: ${isActive} + defaultBillingAddress: { + companyName: "${address.companyName}" + streetAddress1: "${address.streetAddress1}" + streetAddress2: "${address.streetAddress2}" + city: "${address.city}" + postalCode: "${address.postalCode}" + country: ${address.country} + phone: "${address.phone}" + } + defaultShippingAddress: { + companyName: "${address.companyName}" + streetAddress1: "${address.streetAddress1}" + streetAddress2: "${address.streetAddress2}" + city: "${address.city}" + postalCode: "${address.postalCode}" + country: ${address.country} + phone: "${address.phone}" + } + }){ + user{ + id + email + } + accountErrors{ + code + message + } + } + } + `; + return cy.sendRequestWithQuery(mutation); + } + + deleteCustomers(startsWith) { + this.getCustomers(startsWith).then(resp => { + if (resp.body.data.customers) { + const customers = resp.body.data.customers.edges; + customers.forEach(element => { + if (element.node.email.includes(startsWith)) { + this.deleteCustomer(element.node.id); + } + }); + } + }); + } + + deleteCustomer(customerId) { + const mutation = `mutation{ + customerDelete(id:"${customerId}"){ + accountErrors{ + code + message + } + } + }`; + return cy.sendRequestWithQuery(mutation); + } + + getCustomers(startsWith) { + const query = `query{ + customers(first:100, filter: { + search: "${startsWith}" + }){ + edges{ + node{ + id + email + } + } + } + } + `; + return cy.sendRequestWithQuery(query); + } +} +export default Customer; diff --git a/cypress/apiRequests/HomePage.js b/cypress/apiRequests/HomePage.js new file mode 100644 index 000000000..09026cc03 --- /dev/null +++ b/cypress/apiRequests/HomePage.js @@ -0,0 +1,37 @@ +class HomePage { + getSalesForChannel(channelSlug, period) { + const query = `query{ + ordersTotal(period: ${period}, channel:"${channelSlug}"){ + gross{ + amount + } + } + }`; + return cy.sendRequestWithQuery(query); + } + getOrdersForChannel(channelSlug, created) { + const query = `query{ + orders(created: ${created}, channel:"${channelSlug}"){ + totalCount + } + }`; + return cy.sendRequestWithQuery(query); + } + getOrdersWithStatus(status, channelSlug) { + const query = `query{ + orders(status: ${status}, channel:"${channelSlug}"){ + totalCount + } + }`; + return cy.sendRequestWithQuery(query); + } + getProductsOutOfStock(channelSlug) { + const query = `query{ + products(stockAvailability: OUT_OF_STOCK, channel:"${channelSlug}"){ + totalCount + } + }`; + return cy.sendRequestWithQuery(query); + } +} +export default HomePage; diff --git a/cypress/apiRequests/Order.js b/cypress/apiRequests/Order.js new file mode 100644 index 000000000..0005d344a --- /dev/null +++ b/cypress/apiRequests/Order.js @@ -0,0 +1,60 @@ +class Order { + markOrderAsPaid(orderId) { + const mutation = `mutation{ + orderMarkAsPaid(id:"${orderId}"){ + orderErrors{ + message + } + } + }`; + return cy.sendRequestWithQuery(mutation); + } + + addProductToOrder(orderId, variantId, quantity = 1) { + const mutation = `mutation{ + draftOrderLinesCreate(id:"${orderId}", input:{ + quantity:${quantity} + variantId: "${variantId}" + }){ + orderErrors{ + message + } + } + }`; + return cy.sendRequestWithQuery(mutation); + } + + createDraftOrder(customerId, shippingMethodId, channelId) { + const mutation = ` + mutation{ + draftOrderCreate(input:{ + user:"${customerId}" + shippingMethod:"${shippingMethodId}" + channel: "${channelId}" + }){ + orderErrors{ + message + } + order{ + id + } + } + } + `; + return cy.sendRequestWithQuery(mutation); + } + completeOrder(orderId) { + const mutation = `mutation{ + draftOrderComplete(id:"${orderId}"){ + order{ + id + } + orderErrors{ + message + } + } + }`; + return cy.sendRequestWithQuery(mutation); + } +} +export default Order; diff --git a/cypress/apiRequests/Product.js b/cypress/apiRequests/Product.js index 72e0df27c..09dca8bf6 100644 --- a/cypress/apiRequests/Product.js +++ b/cypress/apiRequests/Product.js @@ -1,87 +1,81 @@ class Product { getFirstProducts(first, search) { - let filter = ""; - if (search) { - filter = `, filter:{ - search:"${search}" - }`; - } + const filter = search + ? `, filter:{ + search:"${search}" + }` + : ""; const query = `query{ - products(first:${first}${filter}){ - edges{ - node{ - id - name - variants{ - id - } - } + products(first:${first}${filter}){ + edges{ + node{ + id + name + variants{ + id } } } - `; - return cy.sendRequestWithQuery(query); + } + } + `; + return cy + .sendRequestWithQuery(query) + .then(resp => resp.body.data.products.edges); } - updateChannelInProduct( - productId, - channelId, - isPublished = true, - isAvailableForPurchase = true, - visibleInListings = true - ) { + updateChannelInProduct(productId, channelId) { const mutation = `mutation{ - productChannelListingUpdate(id:"${productId}", - input:{ - addChannels:{ - channelId:"${channelId}" - isPublished:${isPublished} - isAvailableForPurchase:${isAvailableForPurchase} - visibleInListings:${visibleInListings} - } - }){ - product{ - id - name - } - } - }`; + productChannelListingUpdate(id:"${productId}", + input:{ + addChannels:{ + channelId:"${channelId}" + isPublished:true + isAvailableForPurchase:true + } + }){ + product{ + id + name + } + } + }`; return cy.sendRequestWithQuery(mutation); } updateChannelPriceInVariant(variantId, channelId) { const mutation = `mutation{ - productVariantChannelListingUpdate(id: "${variantId}", input:{ - channelId: "${channelId}" - price: 10 - costPrice: 10 - }){ - productChannelListingErrors{ - message - } - } - }`; + productVariantChannelListingUpdate(id: "${variantId}", input:{ + channelId: "${channelId}" + price: 10 + costPrice: 10 + }){ + productChannelListingErrors{ + message + } + } + }`; return cy.sendRequestWithQuery(mutation); } createProduct(attributeId, name, productType, category) { const mutation = `mutation{ - productCreate(input:{ - attributes:[{ - id:"${attributeId}" - }] - name:"${name}" - productType:"${productType}" - category:"${category}" - }){ - product{ - id - } - productErrors{ - field - message - } - } - }`; + productCreate(input:{ + attributes:[{ + id:"${attributeId}" + }] + name:"${name}" + productType:"${productType}" + category:"${category}" + }){ + product{ + id + } + productErrors{ + field + message + } + } + }`; return cy.sendRequestWithQuery(mutation); } @@ -94,98 +88,92 @@ class Product { price = 1, costPrice = 1 ) { - let channelListings = ""; - let stocks = ""; - if (channelId) { - channelListings = `channelListings:{ + const mutation = `mutation{ + productVariantBulkCreate(product:"${productId}", variants:{ + attributes:[] + sku:"${sku}" + channelListings:{ channelId:"${channelId}" price:"${price}" costPrice:"${costPrice}" - }`; - } - if (warehouseId) { - stocks = `stocks:{ + } + stocks:{ warehouse:"${warehouseId}" quantity:${quantity} - }`; - } - const mutation = `mutation{ - productVariantBulkCreate(product:"${productId}", variants:{ - attributes:[] - sku:"${sku}" - ${channelListings} - ${stocks} - }){ - productVariants{ - id - name - } - bulkProductErrors{ - field - message - } - } - }`; + } + }){ + productVariants{ + id + name + } + bulkProductErrors{ + field + message + } + } + }`; return cy.sendRequestWithQuery(mutation); } createTypeProduct(name, attributeId, slug = name) { const mutation = `mutation{ - productTypeCreate(input:{ - name:"${name}" - slug: "${slug}" - isShippingRequired:true - productAttributes:"${attributeId}" - }){ - productErrors{ - field - message - } - productType{ - id - } - } - }`; + productTypeCreate(input:{ + name:"${name}" + slug: "${slug}" + isShippingRequired:true + productAttributes:"${attributeId}" + }){ + productErrors{ + field + message + } + productType{ + id + } + } + }`; return cy.sendRequestWithQuery(mutation); } deleteProduct(productId) { const mutation = `mutation{ - productDelete(id:"${productId}"){ - productErrors{ - field - message - } - } - }`; + productDelete(id:"${productId}"){ + productErrors{ + field + message + } + } + }`; return cy.sendRequestWithQuery(mutation); } getProductTypes(first, search) { const query = `query{ - productTypes(first:${first}, filter:{ - search:"${search}" - }){ - edges{ - node{ - id - name - } - } + productTypes(first:${first}, filter:{ + search:"${search}" + }){ + edges{ + node{ + id + name } - }`; - return cy.sendRequestWithQuery(query); + } + } + }`; + return cy + .sendRequestWithQuery(query) + .then(resp => resp.body.data.productTypes.edges); } deleteProductType(productTypeId) { const mutation = `mutation{ - productTypeDelete(id:"${productTypeId}"){ - productErrors{ - field - message - } - } - }`; + productTypeDelete(id:"${productTypeId}"){ + productErrors{ + field + message + } + } + }`; return cy.sendRequestWithQuery(mutation); } } diff --git a/cypress/apiRequests/ShippingMethod.js b/cypress/apiRequests/ShippingMethod.js new file mode 100644 index 000000000..6c9deb3de --- /dev/null +++ b/cypress/apiRequests/ShippingMethod.js @@ -0,0 +1,86 @@ +class ShippingMethod { + createShippingRate(name, shippingZone) { + const mutation = ` + mutation{ + shippingPriceCreate(input:{ + name: "${name}" + shippingZone: "${shippingZone}" + type: PRICE + }){ + shippingMethod{ + id + } + } + } + `; + return cy.sendRequestWithQuery(mutation); + } + + createShippingZone(name, country) { + const mutation = ` + mutation{ + shippingZoneCreate(input:{ + name: "${name}" + countries: "${country}" + }){ + shippingZone{ + id + } + } + } + `; + return cy.sendRequestWithQuery(mutation); + } + + addChannelToShippingMethod(shippingRateId, channelId, price) { + const mutation = ` + mutation{ + shippingMethodChannelListingUpdate(id:"${shippingRateId}", input:{ + addChannels: { + channelId:"${channelId}" + price: ${price} + } + }){ + shippingMethod{ + id + } + shippingErrors{ + code + message + } + } + } + `; + return cy.sendRequestWithQuery(mutation); + } + + deleteShippingZone(shippingZoneId) { + const mutation = `mutation{ + shippingZoneDelete(id:"${shippingZoneId}"){ + shippingErrors{ + message + } + } + } + `; + return cy.sendRequestWithQuery(mutation); + } + + getShippingZones() { + const query = `query{ + shippingZones(first:100){ + edges{ + node{ + name + id + } + } + } + } + `; + return cy + .sendRequestWithQuery(query) + .then(resp => resp.body.data.shippingZones.edges); + } +} +export default ShippingMethod; diff --git a/cypress/apiRequests/Warehouse.js b/cypress/apiRequests/Warehouse.js new file mode 100644 index 000000000..8237a9333 --- /dev/null +++ b/cypress/apiRequests/Warehouse.js @@ -0,0 +1,57 @@ +class Warehouse { + createWarehouse(name, shippingZone, address, slug = name) { + const mutation = `mutation{ + createWarehouse(input:{ + name:"${name}" + slug:"${slug}" + shippingZones:"${shippingZone}" + address:{ + streetAddress1: "${address.streetAddress1}" + streetAddress2: "${address.streetAddress2}" + city: "${address.city}" + postalCode: "${address.postalCode}" + country: ${address.country} + phone: "${address.phone}" + } + }){ + warehouseErrors{ + field + message + } + warehouse{ + id + } + } + }`; + return cy.sendRequestWithQuery(mutation); + } + getWarehouses(first, search) { + const query = `query{ + warehouses(first:${first}, filter:{ + search:"${search}" + }){ + edges{ + node{ + id + name + } + } + } + }`; + return cy + .sendRequestWithQuery(query) + .then(resp => resp.body.data.warehouses.edges); + } + deleteWarehouse(warehouseId) { + const mutation = `mutation{ + deleteWarehouse(id:"${warehouseId}"){ + warehouseErrors{ + field + message + } + } + }`; + return cy.sendRequestWithQuery(mutation); + } +} +export default Warehouse; diff --git a/cypress/apiRequests/frontShop/Collections.js b/cypress/apiRequests/frontShop/Collections.js index 43719b771..183f0c506 100644 --- a/cypress/apiRequests/frontShop/Collections.js +++ b/cypress/apiRequests/frontShop/Collections.js @@ -14,5 +14,36 @@ class Collections { "query Collection($id: ID!, $channel: String!) {\n collection(id: $id, channel: $channel) {\n id\n slug\n name\n seoDescription\n seoTitle\n backgroundImage {\n url\n __typename\n }\n __typename\n }\n attributes(filter: {channel: $channel, inCollection: $id, filterableInStorefront: true}, first: 100) {\n edges {\n node {\n id\n name\n slug\n values {\n id\n name\n slug\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n}\n"; return cy.sendFrontShopRequest(operationName, query, variables); } + getCollections(search) { + const filter = search + ? `, filter:{ + search:"" + }` + : ""; + const query = `query{ + collections(first:100 ${filter}){ + edges{ + node{ + id + } + } + } + }`; + return cy.sendRequestWithQuery(query); + } + deleteCollection(collectionId) { + const mutation = `mutation{ + collectionDelete(id:"${collectionId}"){ + collection{ + id + } + collectionErrors{ + field + message + } + } + }`; + return cy.sendRequestWithQuery(mutation); + } } export default Collections; diff --git a/cypress/elements/homePage/homePage-selectors.js b/cypress/elements/homePage/homePage-selectors.js new file mode 100644 index 000000000..221cb68e8 --- /dev/null +++ b/cypress/elements/homePage/homePage-selectors.js @@ -0,0 +1,9 @@ +export const HOMEPAGE_SELECTORS = { + sales: "[data-test-id='sales-analytics']", + orders: "[data-test-id='orders-analytics']", + activity: "[data-test-id='activity-card']", + topProducts: "[data-test-id='top-products']", + ordersReadyToFulfill: "[data-test-id='orders-to-fulfill']", + paymentsWaitingForCapture: "[data-test-id='orders-to-capture']", + productsOutOfStock: "[data-test-id='products-out-of-stock']" +}; diff --git a/cypress/fixtures/addresses.json b/cypress/fixtures/addresses.json new file mode 100644 index 000000000..08fad0466 --- /dev/null +++ b/cypress/fixtures/addresses.json @@ -0,0 +1,13 @@ +{ + "plAddress": { + "companyName": "Test3", + "streetAddress1": "Smolna", + "streetAddress2": "13/1", + "city": "Wrocław", + "postalCode": "53-346", + "country": "PL", + "countryArea": "Dolny Śląsk", + "phone": "123456787", + "currency": "PLN" + } +} \ No newline at end of file diff --git a/cypress/integration/channels.js b/cypress/integration/channels.js index 236a7904d..bfee7abae 100644 --- a/cypress/integration/channels.js +++ b/cypress/integration/channels.js @@ -14,16 +14,18 @@ import { ORDERS_SELECTORS } from "../elements/orders/orders-selectors"; import { BUTTON_SELECTORS } from "../elements/shared/button-selectors"; import ChannelsSteps from "../steps/channelsSteps"; import { urlList } from "../url/urlList"; +import ChannelsUtils from "../utils/channelsUtils"; describe("Channels", () => { const channelStartsWith = "Cypress:"; const currency = "PLN"; const channels = new Channels(); + const channelsUtils = new ChannelsUtils(); const channelsSteps = new ChannelsSteps(); before(() => { cy.clearSessionData().loginUserViaRequest(); - channels.deleteTestChannels(channelStartsWith); + channelsUtils.deleteChannels(channelStartsWith); }); beforeEach(() => { @@ -49,19 +51,18 @@ describe("Channels", () => { .get(ADD_CHANNEL_FORM_SELECTORS.backToChannelsList) .click() .get(CHANNELS_SELECTORS.channelsTable) - .contains(randomChannel) - + .contains(randomChannel); + // new channel should be visible in channel selector cy.visit(urlList.homePage) .get(HEADER_SELECTORS.channelSelect) .click() .get(HEADER_SELECTORS.channelSelectList) .contains(randomChannel) - .click() - + .click(); + // new channel should be visible at product availability form - cy.visit(urlList.products) - .waitForGraph("InitialProductFilterData"); + cy.visit(urlList.products).waitForGraph("InitialProductFilterData"); cy.get(PRODUCTS_SELECTORS.productsList) .first() .click() diff --git a/cypress/integration/collections.js b/cypress/integration/collections.js index a3ea0d8aa..7245634b5 100644 --- a/cypress/integration/collections.js +++ b/cypress/integration/collections.js @@ -13,7 +13,7 @@ import ProductsUtils from "../utils/productsUtils"; describe("Collections", () => { const channelsUtils = new ChannelsUtils(); const productsUtils = new ProductsUtils(); - const collectionsFrontUtils = new CollectionsUtils(); + const collectionsUtils = new CollectionsUtils(); const startsWith = "Cy-"; const name = `${startsWith}${faker.random.number()}`; @@ -22,6 +22,9 @@ describe("Collections", () => { before(() => { cy.clearSessionData().loginUserViaRequest(); + productsUtils.deleteProperProducts(startsWith); + // collectionsUtils.deleteProperCollections(startsWith); + channelsUtils .getDefaultChannel() .then(channel => { @@ -78,7 +81,7 @@ describe("Collections", () => { .waitForGraph("CreateCollection") .get(COLLECTION_SELECTORS.saveButton) .click(); - collectionsFrontUtils.getCreatedCollection().then(collection => { + collectionsUtils.getCreatedCollection().then(collection => { cy.get(COLLECTION_SELECTORS.addProductButton) .click() .get(ASSIGN_PRODUCTS_SELECTORS.searchInput) @@ -89,10 +92,9 @@ describe("Collections", () => { .get(ASSIGN_PRODUCTS_SELECTORS.submitButton) .click() .loginInShop(); - collectionsFrontUtils.isCollectionVisible( - collection.id, - defaultChannel.slug - ); + collectionsUtils + .isCollectionVisible(collection.id, defaultChannel.slug) + .then(isVisible => expect(isVisible).to.equal(true)); }); }); // xit("should display collections", () => { diff --git a/cypress/integration/homePage.js b/cypress/integration/homePage.js new file mode 100644 index 000000000..74e8762a1 --- /dev/null +++ b/cypress/integration/homePage.js @@ -0,0 +1,235 @@ +import faker from "faker"; + +import Customer from "../apiRequests/Customer"; +import { HOMEPAGE_SELECTORS } from "../elements/homePage/homePage-selectors"; +import HomePageSteps from "../steps/homePageSteps"; +import { urlList } from "../url/urlList"; +import ChannelsUtils from "../utils/channelsUtils"; +import HomePageUtils from "../utils/homePageUtils"; +import OrdersUtils from "../utils/ordersUtils"; +import ProductsUtils from "../utils/productsUtils"; +import ShippingUtils from "../utils/shippingUtils"; + +// +describe("User authorization", () => { + const startsWith = "Cy-"; + + const customer = new Customer(); + const productsUtils = new ProductsUtils(); + const shippingUtils = new ShippingUtils(); + const ordersUtils = new OrdersUtils(); + const channelsUtils = new ChannelsUtils(); + const homePageUtils = new HomePageUtils(); + const homePageSteps = new HomePageSteps(); + + let customerId; + let defaultChannel; + const productPrice = 22; + const shippingPrice = 12; + const randomName = startsWith + faker.random.number(); + const randomEmail = randomName + "@example.com"; + + before(() => { + cy.clearSessionData().loginUserViaRequest(); + productsUtils.deleteProperProducts(startsWith); + customer.deleteCustomers(startsWith); + shippingUtils.deleteShipping(startsWith); + let addresses; + + channelsUtils + .getDefaultChannel() + .then(channel => { + defaultChannel = channel; + cy.fixture("addresses"); + }) + .then(addressesFixture => (addresses = addressesFixture)) + .then(() => + customer.createCustomer(randomEmail, randomName, addresses.plAddress) + ) + .then(resp => { + customerId = resp.body.data.customerCreate.user.id; + shippingUtils.createShipping( + defaultChannel.id, + randomName, + addresses.plAddress, + shippingPrice + ); + }) + .then(() => { + productsUtils.createTypeAttributeAndCategoryForProduct(randomName); + }) + .then(() => { + const warehouse = shippingUtils.getWarehouse(); + const productType = productsUtils.getProductType(); + const attribute = productsUtils.getAttribute(); + const category = productsUtils.getCategory(); + productsUtils.createProductInChannel( + randomName, + defaultChannel.id, + warehouse.id, + 20, + productType.id, + attribute.id, + category.id, + productPrice + ); + }); + }); + + beforeEach(() => { + cy.clearSessionData().loginUserViaRequest(); + }); + + it("should all elements be visible on the dashboard", () => { + cy.visit(urlList.homePage) + .softAssertVisibility(HOMEPAGE_SELECTORS.sales) + .softAssertVisibility(HOMEPAGE_SELECTORS.orders) + .softAssertVisibility(HOMEPAGE_SELECTORS.activity) + .softAssertVisibility(HOMEPAGE_SELECTORS.topProducts) + .softAssertVisibility(HOMEPAGE_SELECTORS.ordersReadyToFulfill) + .softAssertVisibility(HOMEPAGE_SELECTORS.paymentsWaitingForCapture) + .softAssertVisibility(HOMEPAGE_SELECTORS.productsOutOfStock); + }); + + it("should correct amount of ready to fullfil orders be displayed", () => { + homePageUtils + .getOrdersReadyToFulfill(defaultChannel.slug) + .as("ordersReadyToFulfill"); + + ordersUtils.createReadyToFulfillOrder( + customerId, + shippingUtils.getShippingMethod().id, + defaultChannel.id, + productsUtils.getCreatedVariants() + ); + cy.get("@ordersReadyToFulfill").then(ordersReadyToFulfillBefore => { + const allOrdersReadyToFulfill = ordersReadyToFulfillBefore + 1; + const notANumberRegex = "\\D*"; + const ordersReadyToFulfillRegexp = new RegExp( + `${notANumberRegex}${allOrdersReadyToFulfill}${notANumberRegex}` + ); + cy.visit(urlList.homePage); + homePageSteps.changeChannel(defaultChannel.name); + cy.contains( + HOMEPAGE_SELECTORS.ordersReadyToFulfill, + ordersReadyToFulfillRegexp + ).should("be.visible"); + }); + }); + it("should correct amount of payments waiting for capture be displayed", () => { + homePageUtils + .getOrdersReadyForCapture(defaultChannel.slug) + .as("ordersReadyForCapture"); + const variantsList = productsUtils.getCreatedVariants(); + + ordersUtils.createWaitingForCaptureOrder( + defaultChannel.slug, + randomEmail, + variantsList, + shippingUtils.getShippingMethod().id + ); + + cy.get("@ordersReadyForCapture").then(ordersReadyForCaptureBefore => { + const allOrdersReadyForCapture = ordersReadyForCaptureBefore + 1; + const notANumberRegex = "\\D*"; + const ordersReadyForCaptureRegexp = new RegExp( + `${notANumberRegex}${allOrdersReadyForCapture}${notANumberRegex}` + ); + cy.visit(urlList.homePage); + homePageSteps.changeChannel(defaultChannel.name); + cy.contains( + HOMEPAGE_SELECTORS.ordersReadyForCapture, + ordersReadyForCaptureRegexp + ).should("be.visible"); + }); + }); + it("should correct amount of products out of stock be displayed", () => { + homePageUtils + .getProductsOutOfStock(defaultChannel.slug) + .as("productsOutOfStock"); + const productOutOfStockRandomName = startsWith + faker.random.number(); + const productsOutOfStockUtils = new ProductsUtils(); + const warehouse = shippingUtils.getWarehouse(); + const productType = productsUtils.getProductType(); + const attribute = productsUtils.getAttribute(); + const category = productsUtils.getCategory(); + + productsOutOfStockUtils.createProductInChannel( + productOutOfStockRandomName, + defaultChannel.id, + warehouse.id, + 0, + productType.id, + attribute.id, + category.id, + productPrice + ); + + cy.get("@productsOutOfStock").then(productsOutOfStockBefore => { + const allProductsOutOfStock = productsOutOfStockBefore + 1; + const notANumberRegex = "\\D*"; + const productsOutOfStockRegexp = new RegExp( + `${notANumberRegex}${allProductsOutOfStock}${notANumberRegex}` + ); + cy.visit(urlList.homePage); + homePageSteps.changeChannel(defaultChannel.name); + cy.contains( + HOMEPAGE_SELECTORS.productsOutOfStock, + productsOutOfStockRegexp + ).should("be.visible"); + }); + }); + it("should correct amount of sales be displayed", () => { + homePageUtils.getSalesAmount(defaultChannel.slug).as("salesAmount"); + + ordersUtils.createReadyToFulfillOrder( + customerId, + shippingUtils.getShippingMethod().id, + defaultChannel.id, + productsUtils.getCreatedVariants() + ); + + cy.get("@salesAmount").then(salesAmount => { + const totalAmount = salesAmount + productPrice; + const totalAmountString = totalAmount.toFixed(2); + const totalAmountIntegerValue = totalAmountString.split(".")[0]; + const totalAmountDecimalValue = totalAmountString.split(".")[1]; + const decimalSeparator = "[,.]"; + const totalAmountIntegerWithThousandsSeparator = totalAmountIntegerValue.replace( + /(\d)(?=(\d{3})+(?!\d))/g, + "1[,.]*" + ); + const totalAmountWithSeparators = `${totalAmountIntegerWithThousandsSeparator}${decimalSeparator}${totalAmountDecimalValue}`; + const notANumberRegex = "\\D*"; + const salesAmountRegexp = new RegExp( + `${notANumberRegex}${totalAmountWithSeparators}${notANumberRegex}` + ); + cy.visit(urlList.homePage); + homePageSteps.changeChannel(defaultChannel.name); + cy.contains(HOMEPAGE_SELECTORS.sales, salesAmountRegexp).should( + "be.visible" + ); + }); + }); + it("should correct amount of orders be displayed", () => { + homePageUtils.getTodaysOrders(defaultChannel.slug).as("todaysOrders"); + + ordersUtils.createReadyToFulfillOrder( + customerId, + shippingUtils.getShippingMethod().id, + defaultChannel.id, + productsUtils.getCreatedVariants() + ); + + cy.get("@todaysOrders").then(ordersBefore => { + const allOrders = ordersBefore + 1; + const notANumberRegex = "\\D*"; + const ordersRegexp = new RegExp( + `${notANumberRegex}${allOrders}${notANumberRegex}` + ); + cy.visit(urlList.homePage); + homePageSteps.changeChannel(defaultChannel.name); + cy.contains(HOMEPAGE_SELECTORS.orders, ordersRegexp).should("be.visible"); + }); + }); +}); diff --git a/cypress/steps/homePageSteps.js b/cypress/steps/homePageSteps.js new file mode 100644 index 000000000..bd53c0972 --- /dev/null +++ b/cypress/steps/homePageSteps.js @@ -0,0 +1,12 @@ +import { HEADER_SELECTORS } from "../elements/header/header-selectors"; +class HomePageSteps { + changeChannel(channelName) { + cy.get(HEADER_SELECTORS.channelSelect) + .click() + .get(HEADER_SELECTORS.channelSelectList) + .contains(channelName) + .click() + .waitForGraph("Home"); + } +} +export default HomePageSteps; diff --git a/cypress/support/deleteElement/index.js b/cypress/support/deleteElement/index.js new file mode 100644 index 000000000..ff15bd8b3 --- /dev/null +++ b/cypress/support/deleteElement/index.js @@ -0,0 +1,18 @@ +Cypress.Commands.add( + "handleDeleteElement", + (element, deleteFunction, startsWith) => { + if (element.node.name.includes(startsWith)) { + deleteFunction(element.node.id); + } + } +); +Cypress.Commands.add( + "deleteProperElements", + (deleteFunction, getFunction, startsWith, name) => { + getFunction(100, startsWith).then(elements => { + elements.forEach(element => { + cy.handleDeleteElement(element, deleteFunction, startsWith); + }); + }); + } +); diff --git a/cypress/support/index.js b/cypress/support/index.js index ddfb59da7..4e9a2361d 100644 --- a/cypress/support/index.js +++ b/cypress/support/index.js @@ -1,4 +1,6 @@ import "./user"; +import "./softAssertions"; +import "./deleteElement/index.js"; import { urlList } from "../url/urlList"; @@ -20,7 +22,7 @@ Cypress.Commands.add("clearSessionData", () => { }); Cypress.Commands.add("waitForGraph", operationName => { - cy.intercept("POST", Cypress.env("API_URI"), req => { + cy.intercept("POST", urlList.apiUri, req => { req.statusCode = 200; const requestBody = req.body; if (Array.isArray(requestBody)) { diff --git a/cypress/support/softAssertions/index.js b/cypress/support/softAssertions/index.js new file mode 100644 index 000000000..559ae921f --- /dev/null +++ b/cypress/support/softAssertions/index.js @@ -0,0 +1,89 @@ +let isSoftAssertion = false; +let errors = []; + +chai.softExpect = function(...args) { + isSoftAssertion = true; + return chai.expect(...args); +}; +chai.softAssert = function(...args) { + isSoftAssertion = true; + return chai.assert(...args); +}; + +const origAssert = chai.Assertion.prototype.assert; +chai.Assertion.prototype.assert = function(...args) { + if (isSoftAssertion) { + try { + origAssert.call(this, ...args); + } catch (error) { + errors.push(error); + } + isSoftAssertion = false; + } else { + origAssert.call(this, ...args); + } +}; + +// monkey-patch `Cypress.log` so that the last `cy.then()` isn't logged to command log +const origLog = Cypress.log; +Cypress.log = function(data) { + if (data && data.error && /soft assertions/i.test(data.error.message)) { + data.error.message = "\n\n\t" + data.error.message + "\n\n"; + throw data.error; + } + return origLog.call(Cypress, ...arguments); +}; + +// monkey-patch `it` callback so we insert `cy.then()` as a last command +// to each test case where we'll assert if there are any soft assertion errors +function itCallback(func) { + func(); + cy.then(() => { + if (errors.length) { + const _ = Cypress._; + let msg = ""; + + if (Cypress.browser.isHeaded) { + msg = "Failed soft assertions... check log above ↑"; + } else { + _.each(errors, error => { + msg += "\n" + error; + }); + + msg = msg.replace(/^/gm, "\t"); + } + + throw new Error(msg); + } + }); +} + +const origIt = window.it; +window.it = (title, func) => { + origIt(title, func && (() => itCallback(func))); +}; +window.it.only = (title, func) => { + origIt.only(title, func && (() => itCallback(func))); +}; +window.it.skip = (title, func) => { + origIt.skip(title, func); +}; + +beforeEach(() => { + errors = []; +}); +afterEach(() => { + errors = []; + isSoftAssertion = false; +}); + +Cypress.Commands.add("softAssertMatch", (selector, regexp) => { + cy.get(selector) + .invoke("text") + .then(text => + chai.softExpect(assert.match(text, regexp, "regexp matches")) + ); +}); +Cypress.Commands.add("softAssertVisibility", selector => { + cy.get(selector).then(element => chai.softExpect(element).to.be.visible); +}); diff --git a/cypress/utils/channelsUtils.js b/cypress/utils/channelsUtils.js index 7d93ae3eb..81af09c5d 100644 --- a/cypress/utils/channelsUtils.js +++ b/cypress/utils/channelsUtils.js @@ -2,6 +2,31 @@ import Channels from "../apiRequests/Channels"; class ChannelsUtils { channels = new Channels(); + + deleteChannels(nameStartsWith) { + this.channels.getChannels().then(resp => { + const channelsArray = new Set(resp.body.data.channels); + if (!channelsArray) { + return; + } + channelsArray.forEach(element => { + if (element.name.startsWith(nameStartsWith)) { + const targetChannels = Array.from(channelsArray).filter(function( + channelElement + ) { + return ( + element.currencyCode === channelElement.currencyCode && + element.id !== channelElement.id + ); + }); + if (targetChannels[0]) { + this.channels.deleteChannel(element.id, targetChannels[0].id); + channelsArray.delete(element); + } + } + }); + }); + } getDefaultChannel() { return this.channels.getChannels().then(resp => { const channelsArray = resp.body.data.channels; diff --git a/cypress/utils/collectionsUtils.js b/cypress/utils/collectionsUtils.js index 51deb3579..41518ceea 100644 --- a/cypress/utils/collectionsUtils.js +++ b/cypress/utils/collectionsUtils.js @@ -2,6 +2,7 @@ import Collections from "../apiRequests/frontShop/Collections"; class CollectionsUtils { collectionsRequest = new Collections(); + isCollectionVisible(collectionId, channelSlug) { return this.collectionsRequest .getCollection(collectionId, channelSlug) @@ -15,5 +16,13 @@ class CollectionsUtils { .wait(`@CreateCollection`) .its("response.body.data.collectionCreate.collection"); } + deleteProperCollections(startsWith) { + cy.deleteProperElements( + this.collectionsRequest.deleteCollection, + this.collectionsRequest.getCollections, + startsWith, + "collection" + ); + } } export default CollectionsUtils; diff --git a/cypress/utils/homePageUtils.js b/cypress/utils/homePageUtils.js new file mode 100644 index 000000000..27d298105 --- /dev/null +++ b/cypress/utils/homePageUtils.js @@ -0,0 +1,30 @@ +import HomePage from "../apiRequests/HomePage"; +class HomePageUtils { + homePage = new HomePage(); + getOrdersReadyToFulfill(channelSlug) { + return this.homePage + .getOrdersWithStatus("READY_TO_FULFILL", channelSlug) + .then(resp => resp.body.data.orders.totalCount); + } + getOrdersReadyForCapture(channelSlug) { + return this.homePage + .getOrdersWithStatus("READY_TO_CAPTURE", channelSlug) + .then(resp => resp.body.data.orders.totalCount); + } + getProductsOutOfStock(channelSlug) { + return this.homePage + .getProductsOutOfStock(channelSlug) + .then(resp => resp.body.data.products.totalCount); + } + getSalesAmount(channelSlug) { + return this.homePage + .getSalesForChannel(channelSlug, "TODAY") + .then(resp => resp.body.data.ordersTotal.gross.amount); + } + getTodaysOrders(channelSlug) { + return this.homePage + .getOrdersForChannel(channelSlug, "TODAY") + .then(resp => resp.body.data.orders.totalCount); + } +} +export default HomePageUtils; diff --git a/cypress/utils/ordersUtils.js b/cypress/utils/ordersUtils.js new file mode 100644 index 000000000..e535624d9 --- /dev/null +++ b/cypress/utils/ordersUtils.js @@ -0,0 +1,60 @@ +import Checkout from "../apiRequests/Checkout"; +import Order from "../apiRequests/Order"; + +class OrdersUtils { + checkoutRequest = new Checkout(); + orderRequest = new Order(); + + checkout; + order; + + createWaitingForCaptureOrder( + channelSlug, + email, + variantsList, + shippingMethodId + ) { + return this.createCheckout(channelSlug, email, variantsList) + .then(() => + this.checkoutRequest.addShippingMethod( + this.checkout.id, + shippingMethodId + ) + ) + .then(() => this.addPayment(this.checkout.id)) + .then(() => this.checkoutRequest.completeCheckout(this.checkout.id)); + } + createReadyToFulfillOrder( + customerId, + shippingMethodId, + channelId, + variantsList + ) { + return this.createDraftOrder(customerId, shippingMethodId, channelId) + .then(() => { + variantsList.forEach(variantElement => { + this.orderRequest.addProductToOrder(this.order.id, variantElement.id); + }); + }) + .then(() => this.orderRequest.markOrderAsPaid(this.order.id)) + .then(() => this.orderRequest.completeOrder(this.order.id)); + } + createDraftOrder(customerId, shippingMethodId, channelId) { + return this.orderRequest + .createDraftOrder(customerId, shippingMethodId, channelId) + .then(resp => (this.order = resp.body.data.draftOrderCreate.order)); + } + createCheckout(channelSlug, email, variantsList) { + return this.checkoutRequest + .createCheckout(channelSlug, email, 1, variantsList) + .then(resp => (this.checkout = resp.body.data.checkoutCreate.checkout)); + } + addPayment(checkoutId) { + return this.checkoutRequest.addPayment( + checkoutId, + "mirumee.payments.dummy", + "not-charged" + ); + } +} +export default OrdersUtils; diff --git a/cypress/utils/shippingUtils.js b/cypress/utils/shippingUtils.js new file mode 100644 index 000000000..1fb352907 --- /dev/null +++ b/cypress/utils/shippingUtils.js @@ -0,0 +1,77 @@ +import ShippingMethod from "../apiRequests/ShippingMethod"; +import Warehouse from "../apiRequests/Warehouse"; +class ShippingUtils { + shippingMethodRequest = new ShippingMethod(); + warehouseRequest = new Warehouse(); + + shippingMethod; + shippingZone; + warehouse; + + createShipping(channelId, name, address, price) { + return this.createShippingZone(name, address.country) + .then(() => this.createWarehouse(name, this.shippingZone.id, address)) + .then(() => this.createShippingRate(name, this.shippingZone.id)) + .then(() => + this.shippingMethodRequest.addChannelToShippingMethod( + this.shippingMethod.id, + channelId, + price + ) + ); + } + + createShippingZone(name, country) { + return this.shippingMethodRequest + .createShippingZone(name, country) + .then(resp => { + this.shippingZone = resp.body.data.shippingZoneCreate.shippingZone; + }); + } + createWarehouse(name, shippingZoneId, address) { + return this.warehouseRequest + .createWarehouse(name, shippingZoneId, address) + .then(resp => { + this.warehouse = resp.body.data.createWarehouse.warehouse; + }); + } + createShippingRate(name, shippingZoneId) { + return this.shippingMethodRequest + .createShippingRate(name, shippingZoneId) + .then( + resp => + (this.shippingMethod = + resp.body.data.shippingPriceCreate.shippingMethod) + ); + } + + getShippingMethod() { + return this.shippingMethod; + } + + getShippingZone() { + return this.shippingZone; + } + + getWarehouse() { + return this.warehouse; + } + + deleteShipping(startsWith) { + const shippingMethod = new ShippingMethod(); + const warehouse = new Warehouse(); + cy.deleteProperElements( + shippingMethod.deleteShippingZone, + shippingMethod.getShippingZones, + startsWith, + "shippingZONE" + ); + cy.deleteProperElements( + warehouse.deleteWarehouse, + warehouse.getWarehouses, + startsWith, + "Warehouse" + ); + } +} +export default ShippingUtils; diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index 7ea290962..694aeabbc 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -6767,6 +6767,10 @@ "context": "event", "string": "All events" }, + "src_dot_webhooks_dot_components_dot_WebhookEvents_dot_2745028894": { + "context": "event", + "string": "Page deleted" + }, "src_dot_webhooks_dot_components_dot_WebhookEvents_dot_2862596150": { "context": "event", "string": "Invoice sent" @@ -6795,6 +6799,14 @@ "context": "event", "string": "Order cancelled" }, + "src_dot_webhooks_dot_components_dot_WebhookEvents_dot_3618648517": { + "context": "event", + "string": "Page updated" + }, + "src_dot_webhooks_dot_components_dot_WebhookEvents_dot_3671033983": { + "context": "event", + "string": "Product deleted" + }, "src_dot_webhooks_dot_components_dot_WebhookEvents_dot_3907151399": { "context": "event", "string": "Order fulfilled" @@ -6803,10 +6815,6 @@ "context": "event", "string": "Customer created" }, - "src_dot_webhooks_dot_components_dot_WebhookEvents_dot_40035964": { - "context": "event", - "string": "Changed quantity in checkout" - }, "src_dot_webhooks_dot_components_dot_WebhookEvents_dot_4186057882": { "context": "event", "string": "Invoice requested" @@ -6815,6 +6823,14 @@ "context": "event", "string": "Fulfillment created" }, + "src_dot_webhooks_dot_components_dot_WebhookEvents_dot_679080833": { + "context": "event", + "string": "Page created" + }, + "src_dot_webhooks_dot_components_dot_WebhookEvents_dot_787792649": { + "context": "event", + "string": "Customer updated" + }, "src_dot_webhooks_dot_components_dot_WebhookInfo_dot_1690209105": { "context": "webhook", "string": "Target URL" diff --git a/lws.config.js b/lws.config.js index b8964ecf1..2c01a75be 100644 --- a/lws.config.js +++ b/lws.config.js @@ -1,4 +1,4 @@ module.exports = { - directory: 'build/dashboard/', - port: 9000, -} \ No newline at end of file + directory: "build/dashboard/", + port: 9000 +}; diff --git a/package-lock.json b/package-lock.json index fd6915086..32b6981df 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@material-ui/icons": "^4.5.1", "@material-ui/styles": "^4.5.2", "@saleor/macaw-ui": "^0.1.1-9", + "@types/faker": "^5.1.6", "@sentry/react": "^6.0.0", "apollo": "^2.21.2", "apollo-cache-inmemory": "^1.6.5", @@ -32475,6 +32476,11 @@ "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==", "dev": true }, + "@types/faker": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/@types/faker/-/faker-5.1.6.tgz", + "integrity": "sha512-D+gfFWR/YCvlrYL8lgNZO1jKgIUW+cfhxsgMOqUMYwCI+tl0htD7vCCXp/oJsIxJpxuI7zqmo3gpVQBkFCM4iA==" + }, "@types/fuzzaldrin": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/@types/fuzzaldrin/-/fuzzaldrin-2.1.2.tgz", diff --git a/package.json b/package.json index 9eccdfa2f..bd58a5648 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@material-ui/icons": "^4.5.1", "@material-ui/styles": "^4.5.2", "@saleor/macaw-ui": "^0.1.1-9", + "@types/faker": "^5.1.6", "@sentry/react": "^6.0.0", "apollo": "^2.21.2", "apollo-cache-inmemory": "^1.6.5", diff --git a/schema.graphql b/schema.graphql index 2945ad6ca..e82c2e794 100644 --- a/schema.graphql +++ b/schema.graphql @@ -677,7 +677,7 @@ type Category implements Node & ObjectWithMetadata { level: Int! privateMetadata: [MetadataItem]! metadata: [MetadataItem]! - descriptionJson: String @deprecated(reason: "Will be removed in Saleor 4.0. Use the `description` field instead.") + descriptionJson: JSONString @deprecated(reason: "Will be removed in Saleor 4.0. Use the `description` field instead.") ancestors(before: String, after: String, first: Int, last: Int): CategoryCountableConnection products(channel: String, before: String, after: String, first: Int, last: Int): ProductCountableConnection url: String @deprecated(reason: "This field will be removed after 2020-07-31.") @@ -747,7 +747,7 @@ type CategoryTranslatableContent implements Node { id: ID! name: String! description: JSONString! - descriptionJson: String @deprecated(reason: "Will be removed in Saleor 4.0. Use the `description` field instead.") + descriptionJson: JSONString @deprecated(reason: "Will be removed in Saleor 4.0. Use the `description` field instead.") translation(languageCode: LanguageCodeEnum!): CategoryTranslation category: Category } @@ -765,7 +765,7 @@ type CategoryTranslation implements Node { name: String! description: JSONString! language: LanguageDisplay! - descriptionJson: String @deprecated(reason: "Will be removed in Saleor 4.0. Use the `description` field instead.") + descriptionJson: JSONString @deprecated(reason: "Will be removed in Saleor 4.0. Use the `description` field instead.") } type CategoryUpdate { @@ -1054,7 +1054,7 @@ type Collection implements Node & ObjectWithMetadata { slug: String! privateMetadata: [MetadataItem]! metadata: [MetadataItem]! - descriptionJson: String @deprecated(reason: "Will be removed in Saleor 4.0. Use the `description` field instead.") + descriptionJson: JSONString @deprecated(reason: "Will be removed in Saleor 4.0. Use the `description` field instead.") products(filter: ProductFilterInput, sortBy: ProductOrder, before: String, after: String, first: Int, last: Int): ProductCountableConnection backgroundImage(size: Int): Image translation(languageCode: LanguageCodeEnum!): CollectionTranslation @@ -1206,7 +1206,7 @@ type CollectionTranslatableContent implements Node { id: ID! name: String! description: JSONString! - descriptionJson: String @deprecated(reason: "Will be removed in Saleor 4.0. Use the `description` field instead.") + descriptionJson: JSONString @deprecated(reason: "Will be removed in Saleor 4.0. Use the `description` field instead.") translation(languageCode: LanguageCodeEnum!): CollectionTranslation collection: Collection } @@ -1224,7 +1224,7 @@ type CollectionTranslation implements Node { name: String! description: JSONString! language: LanguageDisplay! - descriptionJson: String @deprecated(reason: "Will be removed in Saleor 4.0. Use the `description` field instead.") + descriptionJson: JSONString @deprecated(reason: "Will be removed in Saleor 4.0. Use the `description` field instead.") } type CollectionUpdate { @@ -2076,10 +2076,6 @@ type GatewayConfigLine { scalar GenericScalar -type Geolocalization { - country: CountryDisplay -} - type GiftCard implements Node { code: String user: User @@ -3319,7 +3315,7 @@ type Page implements Node & ObjectWithMetadata { created: DateTime! privateMetadata: [MetadataItem]! metadata: [MetadataItem]! - contentJson: String! @deprecated(reason: "Will be removed in Saleor 4.0. Use the `content` field instead.") + contentJson: JSONString! @deprecated(reason: "Will be removed in Saleor 4.0. Use the `content` field instead.") translation(languageCode: LanguageCodeEnum!): PageTranslation attributes: [SelectedAttribute!]! } @@ -3446,7 +3442,7 @@ type PageTranslatableContent implements Node { id: ID! title: String! content: JSONString! - contentJson: String @deprecated(reason: "Will be removed in Saleor 4.0. Use the `content` field instead.") + contentJson: JSONString @deprecated(reason: "Will be removed in Saleor 4.0. Use the `content` field instead.") translation(languageCode: LanguageCodeEnum!): PageTranslation page: Page } @@ -3464,7 +3460,7 @@ type PageTranslation implements Node { title: String! content: JSONString! language: LanguageDisplay! - contentJson: String @deprecated(reason: "Will be removed in Saleor 4.0. Use the `content` field instead.") + contentJson: JSONString @deprecated(reason: "Will be removed in Saleor 4.0. Use the `content` field instead.") } input PageTranslationInput { @@ -3657,7 +3653,6 @@ input PaymentInput { gateway: String! token: String amount: PositiveDecimal - billingAddress: AddressInput returnUrl: String } @@ -3850,11 +3845,11 @@ type Product implements Node & ObjectWithMetadata { rating: Float privateMetadata: [MetadataItem]! metadata: [MetadataItem]! - descriptionJson: String @deprecated(reason: "Will be removed in Saleor 4.0. Use the `description` field instead.") + descriptionJson: JSONString @deprecated(reason: "Will be removed in Saleor 4.0. Use the `description` field instead.") url: String! @deprecated(reason: "This field will be removed after 2020-07-31.") thumbnail(size: Int): Image - pricing: ProductPricingInfo - isAvailable: Boolean + pricing(address: AddressInput): ProductPricingInfo + isAvailable(address: AddressInput): Boolean taxType: TaxType attributes: [SelectedAttribute!]! channelListings: [ProductChannelListing!] @@ -3906,7 +3901,7 @@ type ProductChannelListing implements Node { purchaseCost: MoneyRange margin: Margin isAvailableForPurchase: Boolean - pricing: ProductPricingInfo + pricing(address: AddressInput): ProductPricingInfo } input ProductChannelListingAddInput { @@ -4109,6 +4104,7 @@ input ProductOrder { enum ProductOrderField { NAME + RANK PRICE MINIMAL_PRICE DATE @@ -4145,7 +4141,7 @@ type ProductTranslatableContent implements Node { seoDescription: String name: String! description: JSONString! - descriptionJson: String @deprecated(reason: "Will be removed in Saleor 4.0. Use the `description` field instead.") + descriptionJson: JSONString @deprecated(reason: "Will be removed in Saleor 4.0. Use the `description` field instead.") translation(languageCode: LanguageCodeEnum!): ProductTranslation product: Product } @@ -4163,7 +4159,7 @@ type ProductTranslation implements Node { name: String! description: JSONString! language: LanguageDisplay! - descriptionJson: String @deprecated(reason: "Will be removed in Saleor 4.0. Use the `description` field instead.") + descriptionJson: JSONString @deprecated(reason: "Will be removed in Saleor 4.0. Use the `description` field instead.") } type ProductType implements Node & ObjectWithMetadata { @@ -4177,7 +4173,6 @@ type ProductType implements Node & ObjectWithMetadata { privateMetadata: [MetadataItem]! metadata: [MetadataItem]! products(channel: String, before: String, after: String, first: Int, last: Int): ProductCountableConnection @deprecated(reason: "Use the top-level `products` query with the `productTypes` filter.") - taxRate: TaxRateType taxType: TaxType variantAttributes(variantSelection: VariantAttributeScope): [Attribute] productAttributes: [Attribute] @@ -4281,7 +4276,7 @@ type ProductVariant implements Node & ObjectWithMetadata { privateMetadata: [MetadataItem]! metadata: [MetadataItem]! channelListings: [ProductVariantChannelListing!] - pricing: VariantPricingInfo + pricing(address: AddressInput): VariantPricingInfo attributes(variantSelection: VariantAttributeScope): [SelectedAttribute!]! costPrice: Money margin: Int @@ -4290,8 +4285,8 @@ type ProductVariant implements Node & ObjectWithMetadata { images: [ProductImage] translation(languageCode: LanguageCodeEnum!): ProductVariantTranslation digitalContent: DigitalContent - stocks(countryCode: CountryCode): [Stock] - quantityAvailable(countryCode: CountryCode): Int! + stocks(address: AddressInput, countryCode: CountryCode): [Stock] + quantityAvailable(address: AddressInput, countryCode: CountryCode): Int! } type ProductVariantBulkCreate { @@ -4956,7 +4951,6 @@ type Shop { availablePaymentGateways(currency: String): [PaymentGateway!]! availableExternalAuthentications: [ExternalAuthentication!]! availableShippingMethods(channel: String!, address: AddressInput): [ShippingMethod] - geolocalization: Geolocalization countries(languageCode: LanguageCodeEnum): [CountryDisplay!]! defaultCountry: CountryDisplay defaultMailSenderName: String @@ -5833,12 +5827,16 @@ enum WebhookEventTypeEnum { INVOICE_DELETED INVOICE_SENT CUSTOMER_CREATED + CUSTOMER_UPDATED PRODUCT_CREATED PRODUCT_UPDATED - CHECKOUT_QUANTITY_CHANGED + PRODUCT_DELETED CHECKOUT_CREATED CHECKOUT_UPDATED FULFILLMENT_CREATED + PAGE_CREATED + PAGE_UPDATED + PAGE_DELETED } enum WebhookSampleEventTypeEnum { @@ -5852,12 +5850,16 @@ enum WebhookSampleEventTypeEnum { INVOICE_DELETED INVOICE_SENT CUSTOMER_CREATED + CUSTOMER_UPDATED PRODUCT_CREATED PRODUCT_UPDATED - CHECKOUT_QUANTITY_CHANGED + PRODUCT_DELETED CHECKOUT_CREATED CHECKOUT_UPDATED FULFILLMENT_CREATED + PAGE_CREATED + PAGE_UPDATED + PAGE_DELETED } type WebhookUpdate { @@ -5895,4 +5897,4 @@ union _Entity = Address | User | Group | App | ProductVariant | Product | Produc type _Service { sdl: String -} \ No newline at end of file +} diff --git a/src/components/AppLayout/AppChannelContext.tsx b/src/components/AppLayout/AppChannelContext.tsx index 3b479efe7..cf2a16d49 100644 --- a/src/components/AppLayout/AppChannelContext.tsx +++ b/src/components/AppLayout/AppChannelContext.tsx @@ -33,7 +33,7 @@ export const AppChannelProvider: React.FC = ({ children }) => { const [isPickerActive, setPickerActive] = React.useState(false); React.useEffect(() => { - if (!selectedChannel && channelData?.channels) { + if (!selectedChannel && channelData?.channels?.length > 0) { setSelectedChannel(channelData.channels[0].id); } }, [channelData]); diff --git a/src/home/components/HomeActivityCard/HomeActivityCard.tsx b/src/home/components/HomeActivityCard/HomeActivityCard.tsx index 4094a8f35..d8dc07803 100644 --- a/src/home/components/HomeActivityCard/HomeActivityCard.tsx +++ b/src/home/components/HomeActivityCard/HomeActivityCard.tsx @@ -30,16 +30,17 @@ const useStyles = makeStyles( interface HomeActivityCardProps { activities: Home_activities_edges_node[]; + testId?: string; } const HomeActivityCard: React.FC = props => { - const { activities } = props; + const { activities, testId } = props; const classes = useStyles(props); const intl = useIntl(); return ( - + ; title: string; children?: React.ReactNode; } const HomeAnalyticsCard: React.FC = props => { - const { children, title, icon } = props; + const { children, title, icon, testId } = props; const classes = useStyles(props); return ( - +
{title} diff --git a/src/home/components/HomeNotificationTable/HomeNotificationTable.tsx b/src/home/components/HomeNotificationTable/HomeNotificationTable.tsx index 918fd4646..47456b8aa 100644 --- a/src/home/components/HomeNotificationTable/HomeNotificationTable.tsx +++ b/src/home/components/HomeNotificationTable/HomeNotificationTable.tsx @@ -116,7 +116,7 @@ const HomeNotificationTable: React.FC = props => { requiredPermissions={[PermissionEnum.MANAGE_ORDERS]} > - + {ordersToFulfill === undefined ? ( ) : ordersToFulfill === 0 ? ( @@ -136,7 +136,7 @@ const HomeNotificationTable: React.FC = props => { - + {ordersToCapture === undefined ? ( ) : ordersToCapture === 0 ? ( @@ -161,7 +161,7 @@ const HomeNotificationTable: React.FC = props => { requiredPermissions={[PermissionEnum.MANAGE_PRODUCTS]} > - + {productsOutOfStock === undefined ? ( ) : productsOutOfStock === 0 ? ( diff --git a/src/home/components/HomePage/HomePage.tsx b/src/home/components/HomePage/HomePage.tsx index dbae63670..da8d36498 100644 --- a/src/home/components/HomePage/HomePage.tsx +++ b/src/home/components/HomePage/HomePage.tsx @@ -95,6 +95,7 @@ const HomePage: React.FC = props => {
= props => { = props => { ]} > @@ -165,7 +168,10 @@ const HomePage: React.FC = props => { userPermissions={userPermissions} requiredPermissions={[PermissionEnum.MANAGE_ORDERS]} > - +
)} diff --git a/src/home/components/HomeProductListCard/HomeProductListCard.tsx b/src/home/components/HomeProductListCard/HomeProductListCard.tsx index d68cfbe64..13b0c1d6b 100644 --- a/src/home/components/HomeProductListCard/HomeProductListCard.tsx +++ b/src/home/components/HomeProductListCard/HomeProductListCard.tsx @@ -46,18 +46,19 @@ const useStyles = makeStyles( ); interface HomeProductListProps { + testId?: string; topProducts: Home_productTopToday_edges_node[]; onRowClick: (productId: string, variantId: string) => void; } export const HomeProductList: React.FC = props => { - const { topProducts, onRowClick } = props; + const { topProducts, onRowClick, testId } = props; const classes = useStyles(props); const intl = useIntl(); return ( - +
-
- -
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
-
- -
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
-
- -
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
-
- -
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
-
- -
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
-
- -
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
-
- -
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +