diff --git a/.changeset/metal-cows-yawn.md b/.changeset/metal-cows-yawn.md new file mode 100644 index 00000000000..34595fcd04d --- /dev/null +++ b/.changeset/metal-cows-yawn.md @@ -0,0 +1,5 @@ +--- +"saleor-dashboard": minor +--- + +Migrated "TC: SALEOR_26 Create basic info variant - via edit variant page" to playwright diff --git a/cypress/e2e/navigation.js b/cypress/e2e/navigation.js index d66baaa5365..2f5f27b3618 100644 --- a/cypress/e2e/navigation.js +++ b/cypress/e2e/navigation.js @@ -33,7 +33,7 @@ import { expectMainMenuSectionsToBeVisible, } from "../support/pages/mainMenuPage"; -describe("As a staff user I want to navigate through shop using different permissions", () => { +describe("As a staff user I want to navigate through shop using different permissions - migration in progress - to delete when done", () => { it( `should be able to navigate through shop as a staff member using DISCOUNTS permission. TC: SALEOR_3405a - sales list`, { tags: ["@allEnv", "@navigation", "@stable", "@oldRelease", "@critical"] }, diff --git a/playwright/.eslintrc.json b/playwright/.eslintrc.json new file mode 100644 index 00000000000..196a6c648b8 --- /dev/null +++ b/playwright/.eslintrc.json @@ -0,0 +1,8 @@ +{ + "parserOptions": { + "project": "./tsconfig.json" + }, + "rules": { + "@typescript-eslint/no-floating-promises": "error" + } +} diff --git a/playwright/data/testData.ts b/playwright/data/testData.ts index ec729c4100e..d9fdb77f46c 100644 --- a/playwright/data/testData.ts +++ b/playwright/data/testData.ts @@ -3,4 +3,12 @@ export const PRODUCTS = { id: "UHJvZHVjdFR5cGU6Njcy", name: "Single product type", }, + productToAddVariants: { + id: "UHJvZHVjdDo3Mjk%3D", + name: "Product that not contain any variant yet", + }, + productWithOneVariant: { + id: "UHJvZHVjdDo3MzM%3D", + name: "Product that contains single variant", + }, }; diff --git a/playwright/pages/basePage.ts b/playwright/pages/basePage.ts index 79c793f7062..de5758a8195 100644 --- a/playwright/pages/basePage.ts +++ b/playwright/pages/basePage.ts @@ -1,3 +1,4 @@ +import { LOCATORS } from "@data/commonLocators"; import { URL_LIST } from "@data/url"; import type { Locator, Page } from "@playwright/test"; import { expect } from "@playwright/test"; @@ -18,9 +19,18 @@ export class BasePage { ); await expect(this.pageHeader).toBeVisible({ timeout: 10000 }); } + async gotoExistingProductPage(productId: string) { + await this.page.goto(`${URL_LIST.products}${productId}`); + await expect(this.pageHeader).toBeVisible({ timeout: 10000 }); + } async expectGridToBeAttached() { await expect(this.gridCanvas).toBeAttached({ timeout: 10000, }); } + async expectSuccessBanner() { + await expect(this.page.locator(LOCATORS.successBanner)).toBeVisible({ + timeout: 15000, + }); + } } diff --git a/playwright/pages/dialogs/manageProductsChannelAvailability.ts b/playwright/pages/dialogs/manageProductsChannelAvailability.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/playwright/pages/productPage.ts b/playwright/pages/productPage.ts index ae2d5deb8a2..093d725fdaa 100644 --- a/playwright/pages/productPage.ts +++ b/playwright/pages/productPage.ts @@ -1,12 +1,12 @@ import * as faker from "faker"; -import { LOCATORS } from "@data/commonLocators"; import { URL_LIST } from "@data/url"; import { ChannelSelectDialog } from "@pages/dialogs/channelSelectDialog"; import { MetadataSeoPage } from "@pages/pageElements/metadataSeoPage"; import { RightSideDetailsPage } from "@pages/pageElements/rightSideDetailsSection"; import type { Locator, Page } from "@playwright/test"; -import { expect } from "@playwright/test"; + +import { BasePage } from "./basePage"; const productName = `e2e-productName-${faker.datatype.number()}`; const productDescription = `e2e-productDescription-${faker.datatype.number()}`; @@ -42,7 +42,7 @@ export class ProductPage { readonly uploadSavedImagesButton: Locator; readonly uploadMediaUrlButton: Locator; readonly saveUploadUrlButton: Locator; - readonly editVariant: Locator; + readonly editVariantButton: Locator; readonly saveButton: Locator; readonly firstRowDataGrid: Locator; readonly addProductButton: Locator; @@ -50,10 +50,12 @@ export class ProductPage { readonly manageChannelsButton: Locator; metadataSeoPage: MetadataSeoPage; rightSideDetailsPage: RightSideDetailsPage; + basePage: BasePage; channelSelectDialog: ChannelSelectDialog; constructor(page: Page) { this.page = page; + this.basePage = new BasePage(page); this.channelSelectDialog = new ChannelSelectDialog(page); this.metadataSeoPage = new MetadataSeoPage(page); this.rightSideDetailsPage = new RightSideDetailsPage(page); @@ -81,7 +83,7 @@ export class ProductPage { this.uploadSavedImagesButton = page.getByTestId("upload-images"); this.uploadMediaUrlButton = page.getByTestId("upload-media-url"); this.saveUploadUrlButton = page.getByTestId("upload-url-button"); - this.editVariant = page.getByTestId("row-action-button"); + this.editVariantButton = page.getByTestId("row-action-button"); this.productUpdateFormSection = page.getByTestId("product-update-form"); this.firstCategoryItem = page.locator("#downshift-0-item-0"); this.visibleRadioBtn = page.locator("[name='isPublished']"); @@ -150,9 +152,7 @@ export class ProductPage { await this.saveButton.click(); } async expectSuccessBanner() { - await expect(this.page.locator(LOCATORS.successBanner)).toBeVisible({ - timeout: 15000, - }); + await this.basePage.expectSuccessBanner(); } async selectOneChannelAsAvailable() { await this.manageChannelsButton.click(); @@ -164,6 +164,9 @@ export class ProductPage { async clickCreateProductButton() { await this.createProductButton.click(); } + async clickFirstEditVariantButton() { + await this.editVariantButton.first().click(); + } async gotoProductListPage() { await this.page.goto(URL_LIST.products); diff --git a/playwright/pages/productTypePage.ts b/playwright/pages/productTypePage.ts index a1df2ddeda1..7fb66a19ca9 100644 --- a/playwright/pages/productTypePage.ts +++ b/playwright/pages/productTypePage.ts @@ -1,10 +1,11 @@ -import { LOCATORS } from "@data/commonLocators"; import { URL_LIST } from "@data/url"; import type { Locator, Page } from "@playwright/test"; -import { expect } from "@playwright/test"; + +import { BasePage } from "./basePage"; export class ProductTypePage { readonly page: Page; + basePage: BasePage; readonly nameInput: Locator; readonly isShippingRequired: Locator; readonly assignProductAttributeButton: Locator; @@ -18,6 +19,7 @@ export class ProductTypePage { constructor(page: Page) { this.page = page; + this.basePage = new BasePage(page); this.addProductTypeButton = page.getByTestId("add-product-type"); this.notificationSuccess = page.getByTestId("notification-message"); this.nameInput = page.locator("[name='name']"); @@ -53,7 +55,7 @@ export class ProductTypePage { await this.page.goto(URL_LIST.productTypesAdd); } async expectSuccessBanner() { - await expect(this.page.locator(LOCATORS.successBanner)).toBeVisible(); + await this.basePage.expectSuccessBanner(); } async gotoProductTypeListPage() { diff --git a/playwright/pages/variantsPage.ts b/playwright/pages/variantsPage.ts new file mode 100644 index 00000000000..75311cad824 --- /dev/null +++ b/playwright/pages/variantsPage.ts @@ -0,0 +1,129 @@ +import type { Locator, Page } from "@playwright/test"; + +import { BasePage } from "./basePage"; +import { ChannelSelectDialog } from "./dialogs/channelSelectDialog"; +import { MetadataSeoPage } from "./pageElements/metadataSeoPage"; + +export class VariantsPage { + readonly page: Page; + readonly variantNameInput: Locator; + readonly skuTextField: Locator; + readonly attributeOption: Locator; + readonly attributeSelector: Locator; + readonly addWarehouseButton: Locator; + readonly warehouseOption: Locator; + readonly saveButton: Locator; + readonly stockInput: Locator; + readonly booleanAttributeCheckbox: Locator; + readonly selectOption: Locator; + readonly manageChannels: Locator; + readonly allChannels: Locator; + readonly chooseMediaButton: Locator; + readonly assignWarehouseButton: Locator; + readonly addVariantButton: Locator; + readonly priceFieldInput: Locator; + readonly variantsList: Locator; + readonly variantsNames: Locator; + readonly checkoutLimitInput: Locator; + readonly shippingWeightInput: Locator; + channelSelectDialog: ChannelSelectDialog; + metadataSeoPage: MetadataSeoPage; + basePage: BasePage; + + constructor(page: Page) { + this.page = page; + this.basePage = new BasePage(page); + this.metadataSeoPage = new MetadataSeoPage(page); + this.channelSelectDialog = new ChannelSelectDialog(page); + this.variantNameInput = page.getByTestId("variant-name-input"); + this.skuTextField = page.getByTestId("sku"); + this.variantsList = page.getByTestId("variants-list"); + this.variantsNames = page.getByTestId("variant-name"); + this.attributeOption = page.getByTestId("select-option"); + this.attributeSelector = page.getByTestId("attribute-value"); + this.addWarehouseButton = page.getByTestId("add-warehouse"); + this.chooseMediaButton = page.getByTestId("choose-media-button"); + this.addVariantButton = page.getByTestId("button-add-variant"); + this.warehouseOption = page.getByRole("menuitem"); + this.saveButton = page.getByTestId("button-bar-confirm"); + this.stockInput = page.getByTestId("stock-input"); + this.shippingWeightInput = page.locator("[name='weight']"); + this.priceFieldInput = page.getByTestId("price-field"); + this.checkoutLimitInput = page.getByTestId("checkout-limit-input"); + this.assignWarehouseButton = page.getByTestId("assign-warehouse-button"); + this.booleanAttributeCheckbox = page.locator( + "[name*='attribute'][type='checkbox']", + ); + this.selectOption = page.getByTestId("multi-autocomplete-select-option"); + this.manageChannels = page.getByTestId("manage-channels-button"); + this.allChannels = page.locator("[name='allChannels']"); + } + + async typeVariantName(variantName = "XXL beverage") { + await this.variantNameInput.fill(variantName); + } + async typeShippingWeight(weight = "150") { + await this.shippingWeightInput.fill(weight); + } + async typeCheckoutLimit(checkoutLimit = "10") { + await this.checkoutLimitInput.fill(checkoutLimit); + } + async typeSellingPriceInChannel( + channelName: string, + sellingPriceValue = "99", + ) { + await this.page + .locator(`[data-test-id="Channel-${channelName}"]`) + .locator(this.priceFieldInput) + .first() + .fill(sellingPriceValue); + } + async typeCostPriceInChannel(channelName: string, costPriceValue = "10") { + await this.page + .locator(`[data-test-id="Channel-${channelName}"]`) + .locator(this.priceFieldInput) + .last() + .fill(costPriceValue); + } + + async clickMageChannelsButton() { + await this.manageChannels.click(); + } + async clickChooseMediaButton() { + await this.chooseMediaButton.click(); + } + async clickAddVariantButton() { + await this.addVariantButton.click(); + } + + async typeSku(sku = "sku dummy e2e") { + await this.skuTextField.fill(sku); + } + async clickAssignWarehouseButton() { + await this.assignWarehouseButton.click(); + } + async clickSaveVariantButton() { + await this.saveButton.click(); + } + async expectSuccessBanner() { + await this.basePage.expectSuccessBanner(); + } + async selectFirstAttributeValue() { + await this.attributeSelector.click(); + await this.attributeOption.first().click(); + } + async selectWarehouse(warehouse = "Oceania") { + await this.clickAssignWarehouseButton(); + await this.warehouseOption.locator(`text=${warehouse}`).click(); + } + async typeQuantityInStock(warehouse = "Oceania", quantity = "10") { + const quantityInput = await this.page + .getByTestId(warehouse) + .locator(this.stockInput); + await quantityInput.clear(); + await quantityInput.fill(quantity); + } + async addAllMetaData() { + await this.metadataSeoPage.expandAndAddAllMetadata(); + } +} diff --git a/playwright/tests/product.spec.ts b/playwright/tests/product.spec.ts index 92a527fd816..f50a70269ec 100644 --- a/playwright/tests/product.spec.ts +++ b/playwright/tests/product.spec.ts @@ -2,7 +2,8 @@ import { PRODUCTS } from "@data/testData"; import { BasePage } from "@pages/basePage"; import { ProductCreateDialog } from "@pages/dialogs/productCreateDialog"; import { ProductPage } from "@pages/productPage"; -import { test } from "@playwright/test"; +import { VariantsPage } from "@pages/variantsPage"; +import { expect, test } from "@playwright/test"; test.use({ storageState: "playwright/.auth/admin.json" }); @@ -42,3 +43,64 @@ test("TC: SALEOR_5 Create basic product without variants @basic-regression @prod await productPage.clickSaveButton(); await productPage.expectSuccessBanner(); }); + +test("TC: SALEOR_26 Create basic info variant - via edit variant page @basic-regression @product", async ({ + page, +}) => { + const variantName = `TC: SALEOR_26 - variant name - ${new Date().toISOString()}`; + const basePage = new BasePage(page); + const productPage = new ProductPage(page); + const variantsPage = new VariantsPage(page); + + await basePage.gotoExistingProductPage(PRODUCTS.productWithOneVariant.id); + await productPage.clickFirstEditVariantButton(); + await variantsPage.clickAddVariantButton(); + await variantsPage.typeVariantName(variantName); + await variantsPage.clickMageChannelsButton(); + await variantsPage.channelSelectDialog.clickAllChannelsCheckbox(); + await variantsPage.channelSelectDialog.selectFirstChannel(); + await variantsPage.channelSelectDialog.clickConfirmButton(); + await variantsPage.typeSellingPriceInChannel("PLN"); + await variantsPage.typeCostPriceInChannel("PLN"); + await variantsPage.clickSaveVariantButton(); + await variantsPage.expectSuccessBanner(); + await expect( + variantsPage.variantsList.locator(variantsPage.variantsNames, { + hasText: variantName, + }), + ).toBeVisible(); +}); +test("TC: SALEOR_27 Create full info variant - via edit variant page @basic-regression @product", async ({ + page, +}) => { + const variantName = `TC: SALEOR_26 - variant name - ${new Date().toISOString()}`; + const basePage = new BasePage(page); + const productPage = new ProductPage(page); + const variantsPage = new VariantsPage(page); + + await basePage.gotoExistingProductPage(PRODUCTS.productWithOneVariant.id); + await productPage.clickFirstEditVariantButton(); + await variantsPage.clickAddVariantButton(); + await variantsPage.typeVariantName(variantName); + await variantsPage.clickMageChannelsButton(); + await variantsPage.channelSelectDialog.clickAllChannelsCheckbox(); + await variantsPage.channelSelectDialog.selectFirstChannel(); + await variantsPage.channelSelectDialog.clickConfirmButton(); + await variantsPage.selectFirstAttributeValue(); + await variantsPage.typeCheckoutLimit(); + await variantsPage.typeShippingWeight(); + await variantsPage.typeSellingPriceInChannel("PLN"); + await variantsPage.typeSku(); + await variantsPage.addAllMetaData(); + await variantsPage.clickSaveVariantButton(); + await variantsPage.expectSuccessBanner(); + await expect( + variantsPage.variantsList.locator(variantsPage.variantsNames, { + hasText: variantName, + }), + ).toBeVisible(); + await variantsPage.selectWarehouse(); + await variantsPage.typeQuantityInStock(); + await variantsPage.clickSaveVariantButton(); + await variantsPage.expectSuccessBanner(); +}); diff --git a/src/products/components/ProductStocks/ProductStocks.tsx b/src/products/components/ProductStocks/ProductStocks.tsx index 39ca6704a3c..c07ab238336 100644 --- a/src/products/components/ProductStocks/ProductStocks.tsx +++ b/src/products/components/ProductStocks/ProductStocks.tsx @@ -207,7 +207,7 @@ export const ProductStocks: React.FC = ({ ); return ( - + {stock.label} @@ -249,7 +249,12 @@ export const ProductStocks: React.FC = ({ warehousesToAssign.length > 0 && ( - diff --git a/src/products/components/ProductVariantCheckoutSettings/ProductVariantCheckoutSettings.tsx b/src/products/components/ProductVariantCheckoutSettings/ProductVariantCheckoutSettings.tsx index c25890cb111..ea11c953c8d 100644 --- a/src/products/components/ProductVariantCheckoutSettings/ProductVariantCheckoutSettings.tsx +++ b/src/products/components/ProductVariantCheckoutSettings/ProductVariantCheckoutSettings.tsx @@ -37,6 +37,7 @@ const ProductVariantCheckoutSettings: React.FC< {intl.formatMessage(messages.media)} - diff --git a/src/products/components/ProductVariantName/ProductVariantName.tsx b/src/products/components/ProductVariantName/ProductVariantName.tsx index 9d2c3dd9bb2..3deb5474c1a 100644 --- a/src/products/components/ProductVariantName/ProductVariantName.tsx +++ b/src/products/components/ProductVariantName/ProductVariantName.tsx @@ -41,7 +41,7 @@ const ProductVariantName: React.FC = ({ error={!!formErrors.name} disabled={disabled} helperText={getProductErrorMessage(formErrors.name, intl)} - data-test-id="variant-name" + data-test-id="variant-name-input" /> diff --git a/src/products/components/ProductVariantNavigation/ProductVariantNavigation.tsx b/src/products/components/ProductVariantNavigation/ProductVariantNavigation.tsx index 3195b902ca5..fdd0a2890b0 100644 --- a/src/products/components/ProductVariantNavigation/ProductVariantNavigation.tsx +++ b/src/products/components/ProductVariantNavigation/ProductVariantNavigation.tsx @@ -58,7 +58,7 @@ const ProductVariantNavigation: React.FC< {intl.formatMessage(sectionNames.variants)} - + {variants?.length > 0 && } {renderCollection(variants, (variant, variantIndex) => { const isDefault = variant && variant.id === defaultVariantId; @@ -112,7 +112,7 @@ const ProductVariantNavigation: React.FC< )} - + {variant ? variant.name || variant.sku : } {isDefault && (