This commit is contained in:
Karolina Rakoczy 2021-02-19 12:08:10 +01:00
commit 2ea9035e9c
40 changed files with 2846 additions and 601 deletions

View file

@ -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

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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);
}
}

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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']"
};

View file

@ -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"
}
}

View file

@ -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()

View file

@ -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", () => {

View file

@ -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";
// <reference types="cypress" />
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");
});
});
});

View file

@ -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;

View file

@ -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);
});
});
}
);

View file

@ -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)) {

View file

@ -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);
});

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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"

View file

@ -1,4 +1,4 @@
module.exports = {
directory: 'build/dashboard/',
port: 9000,
}
directory: "build/dashboard/",
port: 9000
};

6
package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -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
}
}

View file

@ -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]);

View file

@ -30,16 +30,17 @@ const useStyles = makeStyles(
interface HomeActivityCardProps {
activities: Home_activities_edges_node[];
testId?: string;
}
const HomeActivityCard: React.FC<HomeActivityCardProps> = props => {
const { activities } = props;
const { activities, testId } = props;
const classes = useStyles(props);
const intl = useIntl();
return (
<Card>
<Card data-test-id={testId}>
<CardTitle
title={intl.formatMessage({
defaultMessage: "Activity",

View file

@ -53,19 +53,20 @@ const useStyles = makeStyles(
);
interface HomeAnalyticsCardProps {
testId?: string;
icon: React.ReactElement<IconProps>;
title: string;
children?: React.ReactNode;
}
const HomeAnalyticsCard: React.FC<HomeAnalyticsCardProps> = props => {
const { children, title, icon } = props;
const { children, title, icon, testId } = props;
const classes = useStyles(props);
return (
<Card className={classes.cardSpacing}>
<CardContent className={classes.cardContent}>
<CardContent className={classes.cardContent} data-test-id={testId}>
<div>
<Typography className={classes.cardTitle} variant="subtitle1">
{title}

View file

@ -116,7 +116,7 @@ const HomeNotificationTable: React.FC<HomeNotificationTableProps> = props => {
requiredPermissions={[PermissionEnum.MANAGE_ORDERS]}
>
<TableRow hover={true} onClick={onOrdersToFulfillClick}>
<TableCell>
<TableCell data-test-id="orders-to-fulfill">
{ordersToFulfill === undefined ? (
<Skeleton />
) : ordersToFulfill === 0 ? (
@ -136,7 +136,7 @@ const HomeNotificationTable: React.FC<HomeNotificationTableProps> = props => {
</TableCell>
</TableRow>
<TableRow hover={true} onClick={onOrdersToCaptureClick}>
<TableCell>
<TableCell data-test-id="orders-to-capture">
{ordersToCapture === undefined ? (
<Skeleton />
) : ordersToCapture === 0 ? (
@ -161,7 +161,7 @@ const HomeNotificationTable: React.FC<HomeNotificationTableProps> = props => {
requiredPermissions={[PermissionEnum.MANAGE_PRODUCTS]}
>
<TableRow hover={true} onClick={onProductsOutOfStockClick}>
<TableCell>
<TableCell data-test-id="products-out-of-stock">
{productsOutOfStock === undefined ? (
<Skeleton />
) : productsOutOfStock === 0 ? (

View file

@ -95,6 +95,7 @@ const HomePage: React.FC<HomePageProps> = props => {
<div className={classes.cardContainer}>
<HomeAnalyticsCard
title={"Sales"}
testId="sales-analytics"
icon={
<Sales
className={classes.icon}
@ -113,6 +114,7 @@ const HomePage: React.FC<HomePageProps> = props => {
</HomeAnalyticsCard>
<HomeAnalyticsCard
title={"Orders"}
testId="orders-analytics"
icon={
<Orders
className={classes.icon}
@ -152,6 +154,7 @@ const HomePage: React.FC<HomePageProps> = props => {
]}
>
<HomeProductListCard
testId="top-products"
onRowClick={onProductClick}
topProducts={topProducts}
/>
@ -165,7 +168,10 @@ const HomePage: React.FC<HomePageProps> = props => {
userPermissions={userPermissions}
requiredPermissions={[PermissionEnum.MANAGE_ORDERS]}
>
<HomeActivityCard activities={activities} />
<HomeActivityCard
activities={activities}
testId="activity-card"
/>
</RequirePermissions>
</div>
)}

View file

@ -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<HomeProductListProps> = props => {
const { topProducts, onRowClick } = props;
const { topProducts, onRowClick, testId } = props;
const classes = useStyles(props);
const intl = useIntl();
return (
<Card>
<Card data-test-id={testId}>
<CardTitle
title={intl.formatMessage({
defaultMessage: "Top Products",

File diff suppressed because it is too large Load diff

View file

@ -804,6 +804,7 @@ export enum ProductOrderField {
PRICE = "PRICE",
PUBLICATION_DATE = "PUBLICATION_DATE",
PUBLISHED = "PUBLISHED",
RANK = "RANK",
RATING = "RATING",
TYPE = "TYPE",
}
@ -967,9 +968,9 @@ export enum WebhookErrorCode {
export enum WebhookEventTypeEnum {
ANY_EVENTS = "ANY_EVENTS",
CHECKOUT_CREATED = "CHECKOUT_CREATED",
CHECKOUT_QUANTITY_CHANGED = "CHECKOUT_QUANTITY_CHANGED",
CHECKOUT_UPDATED = "CHECKOUT_UPDATED",
CUSTOMER_CREATED = "CUSTOMER_CREATED",
CUSTOMER_UPDATED = "CUSTOMER_UPDATED",
FULFILLMENT_CREATED = "FULFILLMENT_CREATED",
INVOICE_DELETED = "INVOICE_DELETED",
INVOICE_REQUESTED = "INVOICE_REQUESTED",
@ -980,7 +981,11 @@ export enum WebhookEventTypeEnum {
ORDER_FULFILLED = "ORDER_FULFILLED",
ORDER_FULLY_PAID = "ORDER_FULLY_PAID",
ORDER_UPDATED = "ORDER_UPDATED",
PAGE_CREATED = "PAGE_CREATED",
PAGE_DELETED = "PAGE_DELETED",
PAGE_UPDATED = "PAGE_UPDATED",
PRODUCT_CREATED = "PRODUCT_CREATED",
PRODUCT_DELETED = "PRODUCT_DELETED",
PRODUCT_UPDATED = "PRODUCT_UPDATED",
}

View file

@ -44,6 +44,10 @@ const WebhookEvents: React.FC<WebhookEventsProps> = ({
defaultMessage: "Customer created",
description: "event"
}),
[WebhookEventTypeEnum.CUSTOMER_UPDATED]: intl.formatMessage({
defaultMessage: "Customer updated",
description: "event"
}),
[WebhookEventTypeEnum.CHECKOUT_CREATED]: intl.formatMessage({
defaultMessage: "Checkout created",
description: "event"
@ -84,8 +88,8 @@ const WebhookEvents: React.FC<WebhookEventsProps> = ({
defaultMessage: "Product updated",
description: "event"
}),
[WebhookEventTypeEnum.CHECKOUT_QUANTITY_CHANGED]: intl.formatMessage({
defaultMessage: "Changed quantity in checkout",
[WebhookEventTypeEnum.PRODUCT_DELETED]: intl.formatMessage({
defaultMessage: "Product deleted",
description: "event"
}),
[WebhookEventTypeEnum.FULFILLMENT_CREATED]: intl.formatMessage({
@ -103,6 +107,18 @@ const WebhookEvents: React.FC<WebhookEventsProps> = ({
[WebhookEventTypeEnum.INVOICE_DELETED]: intl.formatMessage({
defaultMessage: "Invoice deleted",
description: "event"
}),
[WebhookEventTypeEnum.PAGE_CREATED]: intl.formatMessage({
defaultMessage: "Page created",
description: "event"
}),
[WebhookEventTypeEnum.PAGE_UPDATED]: intl.formatMessage({
defaultMessage: "Page updated",
description: "event"
}),
[WebhookEventTypeEnum.PAGE_DELETED]: intl.formatMessage({
defaultMessage: "Page deleted",
description: "event"
})
};