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