diff --git a/.changeset/cold-grapes-compete.md b/.changeset/cold-grapes-compete.md new file mode 100644 index 00000000000..b6306983ea7 --- /dev/null +++ b/.changeset/cold-grapes-compete.md @@ -0,0 +1,5 @@ +--- +"saleor-dashboard": minor +--- + +Adding e2e tests for promotion CRUD diff --git a/playwright/data/commonLocators.ts b/playwright/data/commonLocators.ts index e3d75b3be30..6dc0871f58e 100644 --- a/playwright/data/commonLocators.ts +++ b/playwright/data/commonLocators.ts @@ -1,6 +1,8 @@ export const LOCATORS = { + saveButton: "[data-test-id=\"button-bar-confirm\"]", successBanner: "[data-test-type=\"success\"]", errorBanner: "[data-test-type=\"error\"]", infoBanner: "[data-test-type=\"info\"]", dataGridTable: "[data-testid=\"data-grid-canvas\"]", + deleteButton: "[data-test-id=\"button-bar-delete\"]", }; diff --git a/playwright/data/e2eTestData.ts b/playwright/data/e2eTestData.ts index 38f73cd0788..944605356c5 100644 --- a/playwright/data/e2eTestData.ts +++ b/playwright/data/e2eTestData.ts @@ -1,4 +1,4 @@ -export const VOUCHERS_AND_DISCOUNTS = { +export const VOUCHERS = { vouchers: { voucherToBeEditedWithFreeShipping: { id: "Vm91Y2hlcjoyMDI%3D", @@ -22,6 +22,23 @@ export const VOUCHERS_AND_DISCOUNTS = { }, }, }; +export const DISCOUNTS = { + promotionToBeEdited: { + name: "e2e promotion to be edited", + type: "Catalog", + id: "UHJvbW90aW9uOjI0MGVkZGVkLWYzMTAtNGUzZi1iNTlmLTFlMGFkYWE2ZWFkYg==" +}, +promotionWithoutRulesToBeDeleted: { + id: "UHJvbW90aW9uOjRmNTQwMDc1LTZlZGMtNDI1NC1hY2U2LTQ2MzdlMGYxZWJhOA==", + name: "e2e Order predicate promotion without rules", + type: "Order", + +}, +promotionWithRulesToBeDeleted: { + name: "e2e Catalog predicate promotion with rules", + id: "UHJvbW90aW9uOjY0N2M2MzdhLTZjNTEtNDYxZC05MjQ2LTc0YTY0OGM0ZjAxNA==", +}} + export const CUSTOMER_ADDRESS = { changeBillingAddress: { firstName: "Change Billing Address", diff --git a/playwright/data/url.ts b/playwright/data/url.ts index 9eeac02bfd6..a24e3c6f617 100644 --- a/playwright/data/url.ts +++ b/playwright/data/url.ts @@ -24,7 +24,8 @@ export const URL_LIST = { productsAdd: "add?product-type-id=", productTypes: "product-types/", productTypesAdd: "product-types/add", - sales: "discounts/sales/", + discounts: "discounts/sales/", + discountAddPage: "discounts/sales/add", shippingMethods: "shipping/", siteSettings: "site-settings/", staffMembers: "staff/", diff --git a/playwright/pages/basePage.ts b/playwright/pages/basePage.ts index 3d7a4892d09..83f63ed0209 100644 --- a/playwright/pages/basePage.ts +++ b/playwright/pages/basePage.ts @@ -15,8 +15,10 @@ export class BasePage { .locator('[class="clip-region"]') .locator("textarea"), readonly successBanner = page.locator(LOCATORS.successBanner), + readonly deleteButton = page.locator(LOCATORS.deleteButton), readonly filterButton = page.getByTestId("filters-button"), readonly errorBanner = page.locator(LOCATORS.errorBanner), + readonly saveButton = page.locator(LOCATORS.saveButton), readonly infoBanner = page.locator(LOCATORS.infoBanner), readonly previousPagePaginationButton = page.getByTestId( "button-pagination-back", @@ -47,7 +49,9 @@ export class BasePage { async clickBulkDeleteGridRowsButton() { await this.bulkDeleteGridRowsButton.click(); } - + async clickDeleteButton() { + await this.deleteButton.click(); + } async typeInSearchOnListView(searchItem: string) { await this.searchInputListView.fill(searchItem); } @@ -73,6 +77,9 @@ export class BasePage { timeout: 10000, }); } + async clickSaveButton() { + await this.saveButton.click(); + } async expectSuccessBannerMessage(msg: string) { await this.successBanner .locator(`text=${msg}`) @@ -121,7 +128,7 @@ export class BasePage { if (!fiberKey || !node.parentNode) return null; - /* + /* We seek over the fiber node (hack), ignore typings for it. */ const fiberParent = node.parentNode[ diff --git a/playwright/pages/dialogs/deleteDiscountDialog.ts b/playwright/pages/dialogs/deleteDiscountDialog.ts new file mode 100644 index 00000000000..762e29ab506 --- /dev/null +++ b/playwright/pages/dialogs/deleteDiscountDialog.ts @@ -0,0 +1,18 @@ +import type { Page } from "@playwright/test"; + +export class DeleteDiscountDialog { + readonly page: Page; + + constructor( + page: Page, + readonly deleteButton = page.getByTestId( + "delete-confirmation-button", + ), + ) { + this.page = page; + } + + async clickConfirmDeleteButton() { + await this.deleteButton.click(); + } +} diff --git a/playwright/pages/discountsPage.ts b/playwright/pages/discountsPage.ts index 46453a5af49..fd24a73c3cd 100644 --- a/playwright/pages/discountsPage.ts +++ b/playwright/pages/discountsPage.ts @@ -1,12 +1,106 @@ import type { Page } from "@playwright/test"; +import { URL_LIST } from "@data/url"; +import { DeleteDiscountDialog } from "@dialogs/deleteDiscountDialog"; + +import { BasePage } from "@pages/basePage"; +import { date } from "faker"; + +export class DiscountsPage extends BasePage { + deleteDialog: DeleteDiscountDialog; -export class DiscountsPage { - readonly page: Page; constructor( page: Page, readonly createDiscountButton = page.getByTestId("create-sale"), + readonly discountForm = page.getByTestId("discount-form"), + readonly discountNameInput = page.getByTestId("discount-name-input"), + readonly discountTypeSelect = page.getByTestId("discount-type-select"), + readonly activeDatesSection = page.getByTestId("active-dates-section"), + readonly startDateInput = page.getByTestId("start-date-input"), + readonly startHourInput = page.getByTestId("start-hour-input"), + readonly endDateCheckbox = page.getByTestId("has-end-date"), + readonly endDateInput = page.getByTestId("end-date-input"), + readonly endHourInput = page.getByTestId("end-hour-input"), + readonly discountDescriptionInput = page.getByTestId("rich-text-editor-description"), + readonly addRuleButton = page.getByTestId("add-rule"), + readonly editRuleButton = page.getByTestId("rule-edit-button"), + readonly deleteRuleButton = page.getByTestId("rule-delete-button"), + readonly existingRule = page.getByTestId("added-rule"), + readonly addRuleDialog = page.getByTestId("add-rule-dialog"), + readonly ruleSection = page.getByTestId("rule-list"), ) { - this.page = page; + super(page) + this.deleteDialog = new DeleteDiscountDialog(page); +} + async clickCreateDiscountButton() { + await this.createDiscountButton.click(); + } + + async typeDiscountName(name: string) { await this.discountNameInput.fill(name) } + + async selectDiscountType(type: string) { + await this.discountTypeSelect.click() + await this.page.getByRole("listbox").waitFor({ + state: "visible", + timeout: 10000, + }); + await this.page.getByTestId("select-option").filter({ hasText: type }).click(); + } + + async typePromotionDescription(description: string) { + await this.discountDescriptionInput.locator('[contenteditable="true"]').fill(description); + } + + async typeStartDate() { + await this.startDateInput.fill(date.recent().toISOString().split("T")[0]) + } + + async typeStartHour() { + await this.startHourInput.fill("12:00"); + } + + async clickEndDateCheckbox() { + await this.endDateCheckbox.click(); + } + + async typeEndDate() { + await this.endDateInput.fill(date.future().toISOString().split("T")[0]); + } + async typeEndHour() { + await this.endHourInput.fill("12:00") + } + async gotoListView() { + await this.page.goto(URL_LIST.discounts); + await this.createDiscountButton.waitFor({ + state: "visible", + timeout: 10000, + }); + } + async gotoExistingDiscount(promotionId: string) { + const existingDiscountUrl = `${URL_LIST.discounts}${promotionId}`; + await console.log( + `Navigates to existing discount page: ${existingDiscountUrl}`, + ); + await this.page.goto(existingDiscountUrl); + + await this.discountForm.waitFor({ + state: "visible", + timeout: 10000, + }); + } + + async clickEditRuleButton() { + await this.editRuleButton.click(); + } + async clickDeleteRuleButton() { + await this.deleteRuleButton.click() } + + async openExistingPromotionRuleModal(promotionId: string) { + await this.gotoExistingDiscount(promotionId); + await this.editRuleButton.click(); + await this.addRuleDialog.waitFor({ + state: "visible", + timeout: 10000, + }); } } diff --git a/playwright/tests/discounts.spec.ts b/playwright/tests/discounts.spec.ts new file mode 100644 index 00000000000..5be2f40bab8 --- /dev/null +++ b/playwright/tests/discounts.spec.ts @@ -0,0 +1,70 @@ +import { DISCOUNTS } from "@data/e2eTestData"; +import { DiscountsPage } from "@pages/discountsPage"; +import { expect, test } from "@playwright/test"; +import faker from "faker"; + +test.use({ storageState: "playwright/.auth/admin.json" }); +let discounts: DiscountsPage; + + +test.beforeEach(({ page }) => { + discounts = new DiscountsPage(page); +}); + +const discountType = ['Order', 'Catalog']; +for (const type of discountType) { +test(`TC: SALEOR_97 Create promotion with ${type} predicate @discounts @e2e`, async () => { + const discountName = `${faker.lorem.word()}+${type}`; + await discounts.gotoListView(); + await discounts.clickCreateDiscountButton(); + await discounts.typeDiscountName(discountName); + await discounts.selectDiscountType(type); + await discounts.typePromotionDescription(faker.lorem.sentence()); + await discounts.typeStartDate(); + await discounts.typeStartHour(); + await discounts.clickEndDateCheckbox(); + await discounts.typeEndDate(); + await discounts.typeEndHour(); + await discounts.clickSaveButton(); + await expect(discounts.successBanner).toBeVisible({ timeout: 10000 }); + await expect(discounts.pageHeader).toHaveText(discountName); + await expect(discounts.discountTypeSelect).toHaveText(type); + await expect(discounts.ruleSection).toHaveText("Add your first rule to set up a promotion"); +})}; + + +test(`TC: SALEOR_98 Update existing promotion @discounts @e2e`, async () => { + const newDiscountName = `${faker.lorem.word()}`; + await discounts.gotoExistingDiscount(DISCOUNTS.promotionToBeEdited.id); + await discounts.ruleSection.waitFor({ + state: "visible", + timeout: 10000, + }); + await expect(discounts.discountNameInput).toHaveValue(DISCOUNTS.promotionToBeEdited.name, {timeout: 30000}); + await discounts.discountNameInput.clear(); + await discounts.typeDiscountName(newDiscountName); + await discounts.typePromotionDescription(faker.lorem.sentence()); + await discounts.typeStartDate(); + await discounts.typeStartHour(); + await discounts.clickEndDateCheckbox(); + await expect(discounts.endDateInput).not.toBeAttached(); + await expect(discounts.endHourInput).not.toBeAttached(); + await discounts.clickSaveButton(); + await expect(discounts.successBanner).toBeVisible({ timeout: 10000 }); + await expect(discounts.pageHeader).toHaveText(newDiscountName); + await expect(discounts.discountTypeSelect).toHaveText(DISCOUNTS.promotionToBeEdited.type); + }) + +const promotions = [DISCOUNTS.promotionWithoutRulesToBeDeleted, DISCOUNTS.promotionWithRulesToBeDeleted]; +for (const promotion of promotions) { +test(`TC: SALEOR_99 Delete existing ${promotion.name} @discounts @e2e`, async () => { + await discounts.gotoExistingDiscount(promotion.id); + await discounts.ruleSection.waitFor({ + state: "visible", + timeout: 10000, + }); + await expect(discounts.discountNameInput).toHaveValue(promotion.name, {timeout: 30000}); + await discounts.clickDeleteButton(); + await discounts.deleteDialog.clickConfirmDeleteButton(); + await expect(discounts.successBanner).toBeVisible({ timeout: 10000 }); + })}; diff --git a/playwright/tests/discountAndVouchers.spec.ts b/playwright/tests/vouchers.spec.ts similarity index 91% rename from playwright/tests/discountAndVouchers.spec.ts rename to playwright/tests/vouchers.spec.ts index f29b7cd68b6..087ac526c38 100644 --- a/playwright/tests/discountAndVouchers.spec.ts +++ b/playwright/tests/vouchers.spec.ts @@ -1,5 +1,5 @@ import { AVAILABILITY } from "@data/copy"; -import { VOUCHERS_AND_DISCOUNTS } from "@data/e2eTestData"; +import { VOUCHERS } from "@data/e2eTestData"; import { VouchersPage } from "@pages/vouchersPage"; import { expect, test } from "@playwright/test"; @@ -92,7 +92,7 @@ test("TC: SALEOR_85 Create voucher with manual code and percentage discount @vou test("TC: SALEOR_86 Edit voucher to have free shipping discount @vouchers @e2e", async () => { await vouchersPage.gotoExistingVoucherPage( - VOUCHERS_AND_DISCOUNTS.vouchers.voucherToBeEditedWithFreeShipping.id, + VOUCHERS.vouchers.voucherToBeEditedWithFreeShipping.id, ); await vouchersPage.waitForGrid(); const codesRows = await vouchersPage.getNumberOfGridRows(); @@ -117,7 +117,7 @@ test("TC: SALEOR_86 Edit voucher to have free shipping discount @vouchers @e2e", }); test("TC: SALEOR_87 Edit voucher Usage Limits: used in total, per customer, staff only, code used once @vouchers @e2e", async () => { await vouchersPage.gotoExistingVoucherPage( - VOUCHERS_AND_DISCOUNTS.vouchers.voucherToBeEditedUsageLimits.id, + VOUCHERS.vouchers.voucherToBeEditedUsageLimits.id, ); await vouchersPage.waitForGrid(); @@ -171,7 +171,7 @@ test("TC: SALEOR_89 Create voucher with minimum value of order @vouchers @e2e", }); test("TC: SALEOR_90 Edit voucher minimum quantity of items @vouchers @e2e", async () => { await vouchersPage.gotoExistingVoucherPage( - VOUCHERS_AND_DISCOUNTS.vouchers.voucherToBeEditedMinimumQuantity.id, + VOUCHERS.vouchers.voucherToBeEditedMinimumQuantity.id, ); await vouchersPage.clickMinimumQuantityOfItemsButton(); await vouchersPage.typeMinimumQuantityOfItems("4"); @@ -182,7 +182,7 @@ test("TC: SALEOR_90 Edit voucher minimum quantity of items @vouchers @e2e", asyn test("TC: SALEOR_92 Delete voucher @vouchers @e2e", async () => { await vouchersPage.gotoExistingVoucherPage( - VOUCHERS_AND_DISCOUNTS.vouchers.voucherToBeDeleted.id, + VOUCHERS.vouchers.voucherToBeDeleted.id, ); await vouchersPage.clickDeleteSingleVoucherButton(); @@ -192,15 +192,15 @@ test("TC: SALEOR_92 Delete voucher @vouchers @e2e", async () => { await vouchersPage.waitForGrid(); await expect( await vouchersPage.findRowIndexBasedOnText([ - VOUCHERS_AND_DISCOUNTS.vouchers.voucherToBeDeleted.name, + VOUCHERS.vouchers.voucherToBeDeleted.name, ]), - `Given vouchers: ${VOUCHERS_AND_DISCOUNTS.vouchers.voucherToBeBulkDeleted.names} should be deleted from the list`, + `Given vouchers: ${VOUCHERS.vouchers.voucherToBeBulkDeleted.names} should be deleted from the list`, ).toEqual([]); }); test("TC: SALEOR_93 Bulk delete voucher @vouchers @e2e", async () => { await vouchersPage.gotoVouchersListPage(); await vouchersPage.checkListRowsBasedOnContainingText( - VOUCHERS_AND_DISCOUNTS.vouchers.voucherToBeBulkDeleted.names, + VOUCHERS.vouchers.voucherToBeBulkDeleted.names, ); await vouchersPage.clickBulkDeleteButton(); @@ -209,9 +209,9 @@ test("TC: SALEOR_93 Bulk delete voucher @vouchers @e2e", async () => { await vouchersPage.waitForGrid(); await expect( await vouchersPage.findRowIndexBasedOnText( - VOUCHERS_AND_DISCOUNTS.vouchers.voucherToBeBulkDeleted.names, + VOUCHERS.vouchers.voucherToBeBulkDeleted.names, ), - `Given vouchers: ${VOUCHERS_AND_DISCOUNTS.vouchers.voucherToBeBulkDeleted.names} should be deleted from the list`, + `Given vouchers: ${VOUCHERS.vouchers.voucherToBeBulkDeleted.names} should be deleted from the list`, ).toEqual([]); }); @@ -220,7 +220,7 @@ test.skip("TC: SALEOR_94 Edit voucher - assign voucher to specific category @vou const categoryToBeAssigned = "Accessories"; await vouchersPage.gotoExistingVoucherPage( - VOUCHERS_AND_DISCOUNTS.vouchers + VOUCHERS.vouchers .voucherToBeEditedAssignCategoryProductCollection.id, ); await vouchersPage.clickSpecificProductsButton(); @@ -243,7 +243,7 @@ test("TC:SALEOR_95 Edit voucher - assign voucher to specific collection @vouche const collectionToBeAssigned = "Featured Products"; await vouchersPage.gotoExistingVoucherPage( - VOUCHERS_AND_DISCOUNTS.vouchers + VOUCHERS.vouchers .voucherToBeEditedAssignCategoryProductCollection.id, ); await vouchersPage.clickSpecificProductsButton(); @@ -267,7 +267,7 @@ test("TC: SALEOR_96 Edit voucher - assign voucher to specific product @vouchers const productToBeAssigned = "Bean Juice"; await vouchersPage.gotoExistingVoucherPage( - VOUCHERS_AND_DISCOUNTS.vouchers + VOUCHERS.vouchers .voucherToBeEditedAssignCategoryProductCollection.id, ); await vouchersPage.clickSpecificProductsButton(); diff --git a/src/discounts/components/DiscountDeleteModal/DiscountDeleteModal.tsx b/src/discounts/components/DiscountDeleteModal/DiscountDeleteModal.tsx index d3c23f86e64..ea6876fed68 100644 --- a/src/discounts/components/DiscountDeleteModal/DiscountDeleteModal.tsx +++ b/src/discounts/components/DiscountDeleteModal/DiscountDeleteModal.tsx @@ -40,6 +40,7 @@ export const DiscountDeleteModal = ({ diff --git a/src/discounts/components/DiscountDetailsForm/DiscountDetailsForm.tsx b/src/discounts/components/DiscountDetailsForm/DiscountDetailsForm.tsx index bdd7e5d0d91..b90e82482d7 100644 --- a/src/discounts/components/DiscountDetailsForm/DiscountDetailsForm.tsx +++ b/src/discounts/components/DiscountDetailsForm/DiscountDetailsForm.tsx @@ -94,7 +94,7 @@ export const DiscountDetailsForm = ({ return ( -
+ {children({ rulesErrors, rules, diff --git a/src/discounts/components/DiscountGeneralInfo/DiscountGeneralInfo.tsx b/src/discounts/components/DiscountGeneralInfo/DiscountGeneralInfo.tsx index c4cc29596a5..79e2a8e542f 100644 --- a/src/discounts/components/DiscountGeneralInfo/DiscountGeneralInfo.tsx +++ b/src/discounts/components/DiscountGeneralInfo/DiscountGeneralInfo.tsx @@ -57,6 +57,7 @@ export const DiscountGeneralInfo = ({ + {intl.formatMessage(messages.conditions)} diff --git a/src/discounts/components/DiscountRules/componenets/RuleForm/components/RuleConditions/components/AddConditionsSection/AddConditionsSection.tsx b/src/discounts/components/DiscountRules/componenets/RuleForm/components/RuleConditions/components/AddConditionsSection/AddConditionsSection.tsx index aa95fda6bb0..07bbb9cf47d 100644 --- a/src/discounts/components/DiscountRules/componenets/RuleForm/components/RuleConditions/components/AddConditionsSection/AddConditionsSection.tsx +++ b/src/discounts/components/DiscountRules/componenets/RuleForm/components/RuleConditions/components/AddConditionsSection/AddConditionsSection.tsx @@ -27,6 +27,7 @@ export const AddConditionsSection = ({ alignSelf="start" disabled={disabled} onClick={addCondition} + data-test-id="add-rule-condition-button" > diff --git a/src/discounts/components/DiscountRules/componenets/RuleFormModal/RuleFormModal.tsx b/src/discounts/components/DiscountRules/componenets/RuleFormModal/RuleFormModal.tsx index 38cce81381c..a65945a8d86 100644 --- a/src/discounts/components/DiscountRules/componenets/RuleFormModal/RuleFormModal.tsx +++ b/src/discounts/components/DiscountRules/componenets/RuleFormModal/RuleFormModal.tsx @@ -42,7 +42,7 @@ export const RuleFormModal = ({ return ( - + { return (