diff --git a/.changeset/brown-mayflies-check.md b/.changeset/brown-mayflies-check.md new file mode 100644 index 00000000000..f22d8a52943 --- /dev/null +++ b/.changeset/brown-mayflies-check.md @@ -0,0 +1,5 @@ +--- +"saleor-dashboard": minor +--- + +E2E tests for customer CRUD diff --git a/playwright/data/addresses.ts b/playwright/data/addresses.ts new file mode 100644 index 00000000000..4c0f91a8a62 --- /dev/null +++ b/playwright/data/addresses.ts @@ -0,0 +1,59 @@ +import faker from "faker"; + +export type AddressType = { + addressUK: AddressFieldsType; + addressPL: AddressFieldsType; + addressUS: AddressFieldsType; +}; + +export type AddressFieldsType = { + firstName: string; + lastName: string; + companyName: string; + phone: string; + addressLine1: string; + addressLine2: string; + zip: string; + city: string; + country: string; + countryArea: string; +}; + +export const ADDRESS: AddressType = { + addressUS: { + firstName: faker.name.firstName(), + lastName: faker.name.lastName(), + companyName: faker.company.companyName(), + phone: "+12125771133", + addressLine1: "69 W 9th Street", + addressLine2: faker.address.county(), + city: "New York", + zip: "10001", + country: "United States of America", + countryArea: "New York", + }, + addressPL: { + firstName: faker.name.firstName(), + lastName: faker.name.lastName(), + companyName: faker.company.companyName(), + phone: "+48225042123", + addressLine1: "Teczowa", + addressLine2: "7", + city: "WROCLAW", + zip: "53-601", + country: "Poland", + countryArea: "", + }, + addressUK: { + firstName: faker.name.firstName(), + lastName: faker.name.lastName(), + companyName: faker.company.companyName(), + phone: "+445556667777", + addressLine1: "Albert Street", + addressLine2: "78/2", + city: "Edinburgh", + zip: "EH7 5LR", + country: "United Kingdom", + countryArea: "", + }, +}; diff --git a/playwright/data/e2eTestData.ts b/playwright/data/e2eTestData.ts index ca730cc0c04..eee79a47dcc 100644 --- a/playwright/data/e2eTestData.ts +++ b/playwright/data/e2eTestData.ts @@ -619,3 +619,63 @@ export const PRODUCT_TYPES = { ids: ["UHJvZHVjdFR5cGU6NzAw", "UHJvZHVjdFR5cGU6NzAx"], }, }; +export const CUSTOMERS = { + deleteCustomer: { + id: "VXNlcjoxMzY3", + email: "e2e_customer@delete.com", + }, + editCustomer: { + id: "VXNlcjoxMzY2", + email: "e2e_customer_with_addresses@saleor.io", + note: "simple note", + initialShippingAddress: { + firstName: "e2e_customer_with_addresses", + lastName: "to-be-edited", + companyName: "Saleor", + phone: "+48225042123", + addressLine1: "Teczowa", + addressLine2: "7", + city: "WROCLAW", + zip: "53-601", + country: "Poland", + }, + initialBillingAddress: { + firstName: "address", + lastName: "to-be-deleted", + companyName: "Saleor", + phone: "+48225042123", + addressLine1: "Teczowa", + addressLine2: "7", + city: "WROCLAW", + zip: "53-601", + country: "Poland", + }, + additionalAddress: { + firstName: "Test", + lastName: "Test", + addressLine1: "Nowy Świat", + city: "WARSZAWA", + zip: "00-504", + country: "Poland", + }, + }, + customersToBeBulkDeleted: { + names: [ + "e2e_customer_1 bulk-delete", + "e2e_customer_2 bulk-delete", + "e2e_customer_3 bulk-delete", + ], + }, + customerToBeActivated: { + id: "VXNlcjoxMzY0", + email: "e2e-customer@activate.com", + firstName: "e2e-customer", + lastName: "to-be-activated", + }, + customerToBeDeactivated: { + id: "VXNlcjoxMzY1", + email: "e2e-customer@deactivate.com", + firstName: "e2e-customer", + lastName: "to-be-deactivated", + }, +}; diff --git a/playwright/pages/addressesListPage.ts b/playwright/pages/addressesListPage.ts new file mode 100644 index 00000000000..57cda53faef --- /dev/null +++ b/playwright/pages/addressesListPage.ts @@ -0,0 +1,104 @@ +import { AddressFieldsType } from "@data/addresses"; +import { expect, Page } from "@playwright/test"; + +import { BasePage } from "./basePage"; + +export class AddressesListPage extends BasePage { + constructor( + page: Page, + readonly editAddressButton = page.getByTestId("edit-address"), + readonly deleteAddressButton = page.getByTestId("delete-address"), + readonly addAddressButton = page.getByTestId("add-address"), + readonly addressTypeTitle = page.getByTestId("address-type-title"), + readonly addressCard = page.getByTestId("address-card"), + readonly savedAddress = page.getByTestId("address"), + readonly menageAddressButton = page.getByTestId("manage-addresses"), + readonly showMoreMenuButton = page.getByTestId("show-more-button"), + readonly addAddressDialog = page.getByTestId("add-address-dialog"), + readonly setAsDefaultBilling = page.getByTestId("set-default-billing-address"), + readonly setAsDefaultShipping = page.getByTestId("set-default-shipping-address"), + readonly deleteDialog = page.getByTestId("delete-address-dialog-content"), + readonly companyName = page.getByTestId("company-name"), + readonly addressLines = page.getByTestId("addressLines"), + readonly postalCodeAndCity = page.getByTestId("postal-code-and-city"), + readonly countryAreaAndCountry = page.getByTestId("country-area-and-country"), + readonly name = page.getByTestId("name"), + readonly phone = page.getByTestId("phone"), + ) { + super(page); + } + + async clickShowMoreMenu(addressPart: string) { + await this.addressCard + .filter({ hasText: addressPart }) + .locator(this.showMoreMenuButton) + .click(); + } + + async clickManageAddresses() { + await this.menageAddressButton.click(); + } + + async clickAddAddressButton() { + await this.addAddressButton.click(); + } + + async clickEditAddress() { + await this.editAddressButton.click(); + } + + async clickDeleteAddress() { + await this.deleteAddressButton.click(); + await this.waitForDOMToFullyLoad(); + } + + async setAsDeafultShippingAddress() { + await this.setAsDefaultShipping.click(); + } + + async setAsDeafultBillingAddress() { + await this.setAsDefaultBilling.click(); + } + + async verifyRequiredAddressFields(addressPart: string, address: AddressFieldsType) { + const addressToVerify = await this.savedAddress.filter({ + hasText: addressPart, + }); + const city = address.city.toUpperCase(); + + await expect(addressToVerify.locator(this.name)).toContainText( + `${address.firstName} ${address.lastName}`, + ); + await expect(addressToVerify.locator(this.addressLines)).toContainText(address.addressLine1); + await expect(addressToVerify.locator(this.postalCodeAndCity)).toContainText( + ` ${address.zip} ${city}`, + ); + await expect(addressToVerify.locator(this.countryAreaAndCountry)).toContainText( + address.country, + ); + } + + async verifyPhoneField(addressPart: string, address: AddressFieldsType) { + const addressToVerify = await this.savedAddress.filter({ + hasText: addressPart, + }); + + await expect(addressToVerify.locator(this.phone)).toContainText(address.phone); + } + + async verifyCompanyField(addressPart: string, address: AddressFieldsType) { + const addressToVerify = await this.savedAddress.filter({ + hasText: addressPart, + }); + + await expect(addressToVerify.locator(this.companyName)).toContainText(address.companyName); + } + + async verifyAddressLine2Field(addressPart: string, address: AddressFieldsType) { + const addressToVerify = await this.savedAddress.filter({ + hasText: addressPart, + }); + + await expect(addressToVerify.locator(this.addressLines)).toContainText(address.addressLine2); + } +} diff --git a/playwright/pages/basePage.ts b/playwright/pages/basePage.ts index d170d06d43f..d27b916ea53 100644 --- a/playwright/pages/basePage.ts +++ b/playwright/pages/basePage.ts @@ -26,6 +26,7 @@ export class BasePage { readonly searchInputListView = page.getByTestId("search-input"), readonly emptyDataGridListView = page.getByTestId("empty-data-grid-text"), readonly dialog = page.getByRole("dialog"), + readonly giftCardInTable = page.locator('[href*="/dashboard/gift-cards/.*]'), readonly selectAllCheckbox = page.getByTestId("select-all-checkbox").locator("input"), ) { this.page = page; @@ -63,7 +64,10 @@ export class BasePage { } async typeInSearchOnListView(searchItem: string) { - await this.searchInputListView.fill(searchItem); + await this.waitForNetworkIdle(async () => { + await this.searchInputListView.fill(searchItem); + await this.waitForDOMToFullyLoad(); + }); } async clickNextPageButton() { @@ -95,6 +99,10 @@ export class BasePage { await expect(this.errorBanner, "No error banner should be visible").not.toBeVisible(); } + async expectErrorBannerMessage(msg: string) { + await this.errorBanner.locator(`text=${msg}`).waitFor({ state: "visible", timeout: 10000 }); + } + async expectSuccessBanner() { await this.successBanner.first().waitFor({ state: "visible", timeout: 15000 }); await expect(this.errorBanner, "No error banner should be visible").not.toBeVisible(); @@ -105,7 +113,7 @@ export class BasePage { await expect(this.errorBanner, "No error banner should be visible").not.toBeVisible(); } - async waitForNetworkIdle(action: () => Promise, timeoutMs = 50000) { + async waitForNetworkIdle(action: () => Promise, timeoutMs = 60000) { const responsePromise = this.page.waitForResponse("**/graphql/", { timeout: timeoutMs, }); @@ -146,6 +154,7 @@ export class BasePage { We seek over the fiber node (hack), ignore typings for it. */ const fiberParent = node.parentNode[fiberKey as keyof typeof node.parentNode] as any; + const bounds: { x: number; y: number; width: number; height: number } = fiberParent.pendingProps.children.props.gridRef.current.getBounds(col, row); @@ -163,6 +172,21 @@ export class BasePage { ); } + async checkGridCellTextAndClick( + columnNumber: number, + rowsToCheck: number[], + listToCheck: string[], + ) { + const searchResults = []; + + for (let i = 0; i < rowsToCheck.length; i++) { + const searchResult = await this.getGridCellText(rowsToCheck[i], columnNumber); + + searchResults.push(searchResult); + await expect(searchResult).toEqual(listToCheck[i]); + await this.clickGridCell(columnNumber, rowsToCheck[i]); + } + } /* Example: @@ -197,6 +221,7 @@ export class BasePage { await this.gridCanvas.locator("table tr").first().waitFor({ state: "attached" }); const rowIndexes: number[] = []; + const rows = await this.page.$$eval("table tr", rows => rows.map(row => row.textContent)); for (const searchedText of searchTextArray) { @@ -263,4 +288,15 @@ export class BasePage { async waitForDOMToFullyLoad() { await this.page.waitForLoadState("domcontentloaded", { timeout: 70000 }); } + + async expectElementIsHidden(locator: Locator) { + await locator.first().waitFor({ + state: "hidden", + timeout: 30000, + }); + } + + async waitForCanvasContainsText(text: string) { + await this.gridCanvas.getByText(text).waitFor({ state: "attached", timeout: 50000 }); + } } diff --git a/playwright/pages/customersPage.ts b/playwright/pages/customersPage.ts index c1724f4c51d..d380dba5dd5 100644 --- a/playwright/pages/customersPage.ts +++ b/playwright/pages/customersPage.ts @@ -1,12 +1,88 @@ -import type { Page } from "@playwright/test"; +import { URL_LIST } from "@data/url"; +import { AddressDialog } from "@dialogs/addressDialog"; +import { DeleteDialog } from "@dialogs/deleteDialog"; +import { IssueGiftCardDialog } from "@dialogs/issueGiftCardDialog"; +import { AddressForm } from "@forms/addressForm"; +import { BasePage } from "@pages/basePage"; +import { Page } from "@playwright/test"; -export class CustomersPage { - readonly page: Page; +export class CustomersPage extends BasePage { + readonly addressForm: AddressForm; + + readonly deleteDialog: DeleteDialog; + + readonly issueGiftCardDialog: IssueGiftCardDialog; + + readonly addressDialog: AddressDialog; constructor( page: Page, readonly createCustomerButton = page.getByTestId("create-customer"), + readonly customerFirstNameInput = page.getByTestId("customer-first-name").locator("input"), + readonly customerLastNameInput = page.getByTestId("customer-last-name").locator("input"), + readonly customerEmailInput = page.getByTestId("customer-email").locator("input"), + readonly customerNoteInput = page.getByTestId("customer-note").locator("textarea[name='note']"), + readonly saveButton = page.getByTestId("button-bar-confirm"), + readonly deleteButton = page.getByTestId("button-bar-delete"), + readonly issueNewGiftCardButton = page.getByTestId("issue-new-gift-card"), + readonly emailPageTitleText = page.getByTestId("user-email-title"), + readonly customerActiveCheckbox = page.getByTestId("customer-active-checkbox").locator("input"), ) { - this.page = page; + super(page); + this.addressForm = new AddressForm(page); + this.deleteDialog = new DeleteDialog(page); + this.issueGiftCardDialog = new IssueGiftCardDialog(page); + this.addressDialog = new AddressDialog(page); + } + + async goToCustomersListView() { + await this.waitForNetworkIdle(async () => { + await this.page.goto(URL_LIST.customers); + }); + } + + async searchForCustomer(customer: string) { + await this.searchInputListView.fill(customer); + } + + async gotoCustomerDetailsPage(customerId: string) { + await this.waitForNetworkIdle(async () => { + await this.page.goto(`${URL_LIST.customers}${customerId}`); + }); + } + + async clickOnCreateCustomer() { + await this.createCustomerButton.click(); + } + + async fillFirstAndLastName(firstName: string, lastName: string) { + await this.customerFirstNameInput.fill(firstName, { timeout: 1000000 }); + await this.customerLastNameInput.fill(lastName); + } + + async fillEmail(email: string) { + await this.customerEmailInput.fill(email); + } + + async fillNote(note: string) { + await this.customerNoteInput.fill(note); + } + + async saveCustomer() { + await this.saveButton.click(); + } + + async deleteCustomer() { + await this.deleteButton.click(); + } + + async clickIssueNewGiftCard() { + await this.waitForNetworkIdle(async () => { + await this.issueNewGiftCardButton.click(); + }); + } + + async clickCustomerActiveCheckbox() { + await this.customerActiveCheckbox.click(); } } diff --git a/playwright/pages/dialogs/addAddressDialog.ts b/playwright/pages/dialogs/addAddressDialog.ts new file mode 100644 index 00000000000..b18284973fe --- /dev/null +++ b/playwright/pages/dialogs/addAddressDialog.ts @@ -0,0 +1,27 @@ +import { BasePage } from "@pages/basePage"; +import { AddressForm } from "@pages/forms/addressForm"; +import type { Page } from "@playwright/test"; + +export class AddAddressDialog extends BasePage { + readonly addressForm: AddressForm; + + constructor( + page: Page, + + readonly backButton = page.getByTestId("back"), + readonly submitButton = page.getByTestId("submit"), + ) { + super(page); + this.addressForm = new AddressForm(page); + } + + async clickBackButton() { + await this.backButton.click(); + } + + async clickConfirmButton() { + await this.waitForNetworkIdle(async () => { + await this.submitButton.click(); + }); + } +} diff --git a/playwright/pages/dialogs/addressDialog.ts b/playwright/pages/dialogs/addressDialog.ts index c4a8a4666ed..05aaa2b4bd3 100644 --- a/playwright/pages/dialogs/addressDialog.ts +++ b/playwright/pages/dialogs/addressDialog.ts @@ -1,6 +1,10 @@ import type { Page } from "@playwright/test"; +import { AddressForm } from "../forms/addressForm"; + export class AddressDialog { + readonly addressForm: AddressForm; + constructor( page: Page, readonly newAddressRadioButton = page.getByTestId("newAddress").locator('[value="newAddress"]'), @@ -20,7 +24,9 @@ export class AddressDialog { readonly countrySelect = page.getByTestId("address-edit-country-select-field"), readonly countryAreaSelect = page.getByTestId("address-edit-country-area-field"), readonly selectOptions = page.getByTestId("single-autocomplete-select-option"), - ) {} + ) { + this.addressForm = new AddressForm(page); + } async clickConfirmButton() { await this.submitButton.click(); diff --git a/playwright/pages/dialogs/deleteAddressDialog.ts b/playwright/pages/dialogs/deleteAddressDialog.ts new file mode 100644 index 00000000000..9c320c40c33 --- /dev/null +++ b/playwright/pages/dialogs/deleteAddressDialog.ts @@ -0,0 +1,23 @@ +import { BasePage } from "@pages/basePage"; +import { Page } from "@playwright/test"; + +export class DeleteAddressDialog extends BasePage { + constructor( + page: Page, + + readonly backButton = page.getByTestId("back"), + readonly submitButton = page.getByTestId("submit"), + ) { + super(page); + } + + async clickBackButton() { + await this.backButton.click(); + } + + async clickDeleteButton() { + await this.waitForNetworkIdle(async () => { + await this.submitButton.click(); + }); + } +} diff --git a/playwright/pages/dialogs/deleteDialog.ts b/playwright/pages/dialogs/deleteDialog.ts index 9a13438007f..58477acc769 100644 --- a/playwright/pages/dialogs/deleteDialog.ts +++ b/playwright/pages/dialogs/deleteDialog.ts @@ -12,8 +12,10 @@ export class DeleteDialog extends BasePage { } async clickDeleteButton() { - await this.deleteButton.first().click(); - await this.deleteButton.waitFor({ state: "hidden" }); + await this.waitForNetworkIdle(async () => { + await this.deleteButton.first().click(); + await this.deleteButton.waitFor({ state: "hidden" }); + }); } async clickConfirmDeletionCheckbox() { diff --git a/playwright/pages/dialogs/issueGiftCardDialog.ts b/playwright/pages/dialogs/issueGiftCardDialog.ts index 5b935546796..2d886f14379 100644 --- a/playwright/pages/dialogs/issueGiftCardDialog.ts +++ b/playwright/pages/dialogs/issueGiftCardDialog.ts @@ -1,8 +1,7 @@ +import { BasePage } from "@pages/basePage"; import { Page } from "@playwright/test"; -export class IssueGiftCardDialog { - readonly page: Page; - +export class IssueGiftCardDialog extends BasePage { constructor( page: Page, readonly enterAmountInput = page.locator('[name="balanceAmount"]'), @@ -21,11 +20,13 @@ export class IssueGiftCardDialog { readonly okButton = page.getByTestId("submit"), readonly copyCodeButton = page.getByTestId("copy-code-button"), ) { - this.page = page; + super(page); } async clickIssueButton() { - await this.issueButton.click(); + await this.waitForNetworkIdle(async () => { + await this.issueButton.click(); + }); } async clickOkButton() { @@ -67,4 +68,10 @@ export class IssueGiftCardDialog { async clickRequiresActivationCheckbox() { await this.requiresActivationCheckbox.click(); } + + async getGiftCardCode() { + const allTexts = await this.cardCode.allTextContents(); + + return allTexts[0]; + } } diff --git a/playwright/pages/dialogs/shippingMethodDialog.ts b/playwright/pages/dialogs/shippingMethodDialog.ts index 47592e27053..b510d09b7a8 100644 --- a/playwright/pages/dialogs/shippingMethodDialog.ts +++ b/playwright/pages/dialogs/shippingMethodDialog.ts @@ -1,8 +1,7 @@ +import { BasePage } from "@pages/basePage"; import { expect, Page } from "@playwright/test"; -export class ShippingAddressDialog { - readonly page: Page; - +export class ShippingAddressDialog extends BasePage { constructor( page: Page, readonly selectShippingMethodInput = page.locator('[id="mui-component-select-shippingMethod"]'), @@ -10,11 +9,13 @@ export class ShippingAddressDialog { readonly backButton = page.getByTestId("back"), readonly shippingMethodOption = page.locator("[data-test-id*='select-field-option']"), ) { - this.page = page; + super(page); } async pickAndConfirmFirstShippingMethod() { - await this.selectShippingMethodInput.click(); + await this.waitForNetworkIdle(async () => { + await this.selectShippingMethodInput.click(); + }); await this.shippingMethodOption.first().click(); await this.confirmButton.click(); await expect(this.selectShippingMethodInput).not.toBeVisible(); diff --git a/playwright/pages/draftOrdersPage.ts b/playwright/pages/draftOrdersPage.ts index 0d17e7c583f..b2c93f9ef10 100644 --- a/playwright/pages/draftOrdersPage.ts +++ b/playwright/pages/draftOrdersPage.ts @@ -47,6 +47,7 @@ export class DraftOrdersPage extends BasePage { async goToDraftOrdersListView() { await this.page.goto(URL_LIST.draftOrders); + await this.waitForGrid(); } async clickBulkDeleteButton() { diff --git a/playwright/pages/forms/addressForm.ts b/playwright/pages/forms/addressForm.ts new file mode 100644 index 00000000000..2432ea8e373 --- /dev/null +++ b/playwright/pages/forms/addressForm.ts @@ -0,0 +1,82 @@ +import { AddressFieldsType } from "@data/addresses"; +import { BasePage } from "@pages/basePage"; +import { Page } from "@playwright/test"; + +export class AddressForm extends BasePage { + constructor( + page: Page, + readonly firstNameInput = page.getByTestId("first-name-input").locator("input"), + readonly lastNameInput = page.getByTestId("last-name-input").locator("input"), + readonly companyNameInput = page.getByTestId("company-name-input").locator("input"), + readonly phoneInput = page.getByTestId("phone-input").locator("input"), + readonly cityInput = page.getByTestId("city-input").locator("input"), + readonly zipInput = page.getByTestId("zip-input").locator("input"), + readonly addressLine1Input = page.getByTestId("address-line-1-input").locator("input"), + readonly addressLine2Input = page.getByTestId("address-line-2-input").locator("input"), + readonly countrySelect = page.getByTestId("address-edit-country-select-field").locator("input"), + readonly countryAreaSelect = page + .getByTestId("address-edit-country-area-field") + .locator("input"), + readonly selectOptions = page.getByTestId("single-autocomplete-select-option"), + ) { + super(page); + } + + async typeFirstName(name: string) { + await this.firstNameInput.fill(name); + } + + async typeLastName(lastName: string) { + await this.lastNameInput.fill(lastName); + } + + async typeCompanyName(companyName: string) { + await this.companyNameInput.fill(companyName); + } + + async typePhone(phone: string) { + await this.phoneInput.fill(phone); + } + + async typeAddressLine1(addressLine1: string) { + await this.addressLine1Input.fill(addressLine1); + } + + async typeAddressLine2(addressLine2: string) { + await this.addressLine2Input.fill(addressLine2); + } + + async typeCity(cityName: string) { + await this.cityInput.fill(cityName); + } + + async typeZip(zip: string) { + await this.zipInput.fill(zip); + } + + async selectCountryArea(countryArea: string) { + await this.countryAreaSelect.fill(countryArea); + await this.selectOptions.filter({ hasText: countryArea }).first().click(); + } + + async clickSubmitButton() { + this.page.getByTestId("submit").click(); + } + + async selectCountry(country: string) { + await this.countrySelect.click(); + await this.countrySelect.clear(); + await this.countrySelect.fill(country); + await this.selectOptions.filter({ hasText: country }).first().click(); + } + + async completeBasicInfoAddressForm(address: AddressFieldsType) { + await this.typeFirstName(address.firstName); + await this.typeLastName(address.lastName); + await this.typeAddressLine1(address.addressLine1); + await this.typeCity(address.city); + await this.typeZip(address.zip); + await this.selectCountry(address.country); + await this.waitForDOMToFullyLoad(); + } +} diff --git a/playwright/pages/giftCardsPage.ts b/playwright/pages/giftCardsPage.ts index 7da0c72b179..fbd15dce873 100644 --- a/playwright/pages/giftCardsPage.ts +++ b/playwright/pages/giftCardsPage.ts @@ -86,7 +86,10 @@ export class GiftCardsPage extends BasePage { } async gotoGiftCardsListView() { - await this.page.goto(URL_LIST.giftCards); + await this.waitForNetworkIdle(async () => { + await this.page.goto(URL_LIST.giftCards); + await this.waitForDOMToFullyLoad(); + }); } async gotoExistingGiftCardView(giftCardId: string) { diff --git a/playwright/pages/ordersPage.ts b/playwright/pages/ordersPage.ts index a963294d6f5..78cf9c2699c 100644 --- a/playwright/pages/ordersPage.ts +++ b/playwright/pages/ordersPage.ts @@ -108,5 +108,6 @@ export class OrdersPage extends BasePage { await console.log("Navigating to order details view: " + orderLink); await this.page.goto(orderLink); + await this.waitForGrid(); } } diff --git a/playwright/pages/pageElements/rightSideDetailsSection.ts b/playwright/pages/pageElements/rightSideDetailsSection.ts index 2cc48e4a55b..84030d622cf 100644 --- a/playwright/pages/pageElements/rightSideDetailsSection.ts +++ b/playwright/pages/pageElements/rightSideDetailsSection.ts @@ -1,11 +1,10 @@ +import { BasePage } from "@pages/basePage"; import { ChannelSelectDialog } from "@pages/dialogs/channelSelectDialog"; import { expect, Locator, Page } from "@playwright/test"; -export class RightSideDetailsPage { +export class RightSideDetailsPage extends BasePage { readonly channelSelectDialog: ChannelSelectDialog; - readonly page: Page; - constructor( page: Page, readonly selectWarehouseShippingMethodButton = page.getByTestId( @@ -51,7 +50,7 @@ export class RightSideDetailsPage { readonly warehouseSelect = page.getByTestId("warehouse-auto-complete-select"), readonly allocationHighStockButton = page.getByTestId("PRIORITIZE_HIGH_STOCK"), ) { - this.page = page; + super(page); this.channelSelectDialog = new ChannelSelectDialog(page); } @@ -79,6 +78,7 @@ export class RightSideDetailsPage { async typeAndSelectSingleWarehouseShippingPage(warehouse = "Europe") { await this.selectWarehouseShippingMethodButton.locator("input").fill(warehouse); + await this.selectOption.filter({ hasText: warehouse }).first().click(); // below click hides prompted options this.clickWarehouseSelectShippingPage(); @@ -87,6 +87,7 @@ export class RightSideDetailsPage { async typeAndSelectMultipleWarehousesShippingPage(warehouses: string[]) { for (const warehouse of warehouses) { await this.selectWarehouseShippingMethodButton.locator("input").fill(warehouse); + await this.selectOption.filter({ hasText: warehouse }).first().click(); } this.clickWarehouseSelectShippingPage(); @@ -179,6 +180,7 @@ export class RightSideDetailsPage { async selectCustomer(customer = "allison.freeman@example.com") { await this.selectCustomerOption.locator(`text=${customer}`).click(); + await this.waitForDOMToFullyLoad(); } async selectOneChannelAsAvailableWhenMoreSelected(channel: string) { @@ -192,6 +194,6 @@ export class RightSideDetailsPage { await this.manageChannelsButton.click(); await this.channelSelectDialog.selectChannel(channel); await this.channelSelectDialog.clickConfirmButton(); - await this.page.waitForLoadState("domcontentloaded"); + await this.waitForDOMToFullyLoad(); } } diff --git a/playwright/tests/customers.spec.ts b/playwright/tests/customers.spec.ts new file mode 100644 index 00000000000..9a354a384e7 --- /dev/null +++ b/playwright/tests/customers.spec.ts @@ -0,0 +1,234 @@ +import { ADDRESS } from "@data/addresses"; +import { CUSTOMERS } from "@data/e2eTestData"; +import { AddressesListPage } from "@pages/addressesListPage"; +import { CustomersPage } from "@pages/customersPage"; +import { AddAddressDialog } from "@pages/dialogs/addAddressDialog"; +import { DeleteAddressDialog } from "@pages/dialogs/deleteAddressDialog"; +import { AddressForm } from "@pages/forms/addressForm"; +import { GiftCardsPage } from "@pages/giftCardsPage"; +import { expect, test } from "@playwright/test"; +import faker from "faker"; + +test.use({ storageState: "./playwright/.auth/admin.json" }); + +let customersPage: CustomersPage; +let giftCardsPage: GiftCardsPage; +let addressesListPage: AddressesListPage; +let addressForm: AddressForm; +let deleteAddressDialog: DeleteAddressDialog; +let addAddressDialog: AddAddressDialog; + +test.beforeEach(({ page }) => { + customersPage = new CustomersPage(page); + giftCardsPage = new GiftCardsPage(page); + addressesListPage = new AddressesListPage(page); + addressForm = new AddressForm(page); + addAddressDialog = new AddAddressDialog(page); + deleteAddressDialog = new DeleteAddressDialog(page); +}); + +test("TC: SALEOR_199 Create customer @e2e @customer", async () => { + const firstName = faker.name.firstName(); + const lastName = faker.name.lastName(); + const note = faker.lorem.sentence(); + const email = faker.internet.email(); + + await customersPage.goToCustomersListView(); + await customersPage.clickOnCreateCustomer(); + await customersPage.fillFirstAndLastName(firstName, lastName); + await customersPage.fillEmail(email); + + const newAddress = ADDRESS.addressUS; + + await addressForm.completeBasicInfoAddressForm(newAddress); + await addressForm.typeCompanyName(newAddress.companyName); + await addressForm.typePhone(newAddress.phone); + await addressForm.typeAddressLine2(newAddress.addressLine2); + await addressForm.selectCountryArea(newAddress.countryArea); + await customersPage.fillNote(note); + await customersPage.saveCustomer(); + await customersPage.expectSuccessBanner(); + await expect(customersPage.pageHeader).toContainText(`${firstName} ${lastName}`); + await expect(customersPage.customerNoteInput).toContainText(note); + await expect(customersPage.customerFirstNameInput).toHaveValue(firstName); + await expect(customersPage.customerLastNameInput).toHaveValue(lastName); + await expect(customersPage.customerEmailInput).toHaveValue(email.toLowerCase()); +}); + +test("TC: SALEOR_200 As an admin I should not be able to create customer with duplicated email @e2e @customer", async () => { + const firstName = faker.name.firstName(); + const lastName = faker.name.lastName(); + const note = faker.lorem.sentence(); + + await customersPage.goToCustomersListView(); + await customersPage.clickOnCreateCustomer(); + await customersPage.fillFirstAndLastName(firstName, lastName); + await customersPage.fillEmail(CUSTOMERS.customerToBeDeactivated.email); + + const newAddress = ADDRESS.addressUS; + + await addressForm.completeBasicInfoAddressForm(newAddress); + await addressForm.typeCompanyName(newAddress.companyName); + await addressForm.typePhone(newAddress.phone); + await addressForm.typeAddressLine2(newAddress.addressLine2); + await addressForm.selectCountryArea(newAddress.countryArea); + await customersPage.fillNote(note); + await customersPage.saveCustomer(); + await customersPage.expectErrorBannerMessage("User with this Email already exists."); +}); + +test("TC: SALEOR_201 Update customer account info @e2e @customer", async () => { + const firstName = faker.name.firstName(); + const lastName = faker.name.lastName(); + const email = faker.internet.email(); + const note = faker.lorem.sentence(); + + await customersPage.gotoCustomerDetailsPage(CUSTOMERS.editCustomer.id); + await customersPage.fillNote(note); + await customersPage.fillFirstAndLastName(firstName, lastName); + await customersPage.fillEmail(email); + await customersPage.saveCustomer(); + await customersPage.expectSuccessBanner(); + await expect(customersPage.pageHeader).toContainText(`${firstName} ${lastName}`); + await expect(customersPage.customerNoteInput).toContainText(note); + await expect(customersPage.customerFirstNameInput).toHaveValue(firstName); + await expect(customersPage.customerLastNameInput).toHaveValue(lastName); + await expect(customersPage.customerEmailInput).toHaveValue(email.toLowerCase()); +}); + +test("TC: SALEOR_202 Deactivate a customer @e2e @customer", async () => { + await customersPage.gotoCustomerDetailsPage(CUSTOMERS.customerToBeDeactivated.id); + await customersPage.customerActiveCheckbox.click(); + await customersPage.saveCustomer(); + await customersPage.expectSuccessBanner(); + await expect(customersPage.customerActiveCheckbox).not.toBeChecked(); +}); + +test("TC: SALEOR_203 Activate a customer @e2e @customer", async () => { + await customersPage.gotoCustomerDetailsPage(CUSTOMERS.customerToBeActivated.id); + await customersPage.customerActiveCheckbox.click(); + await customersPage.saveCustomer(); + await customersPage.expectSuccessBanner(); + await expect(customersPage.customerActiveCheckbox).toBeChecked(); +}); + +test("TC: SALEOR_204 Delete customer from the details page @e2e @customer", async () => { + await customersPage.gotoCustomerDetailsPage(CUSTOMERS.deleteCustomer.id); + await customersPage.deleteCustomer(); + await customersPage.deleteDialog.clickDeleteButton(); + await customersPage.expectSuccessBanner(); + await customersPage.goToCustomersListView(); + await customersPage.searchForCustomer(CUSTOMERS.deleteCustomer.email); + await expect(customersPage.emptyDataGridListView).toBeVisible(); +}); + +test("TC: SALEOR_205 Bulk delete customers @e2e @customer", async () => { + const customersToBeBulkDeleted = CUSTOMERS.customersToBeBulkDeleted.names; + + await customersPage.goToCustomersListView(); + await customersPage.typeInSearchOnListView("bulk-delete"); + + const rowsToCheck = [0, 1, 2]; + + await customersPage.checkGridCellTextAndClick(0, rowsToCheck, customersToBeBulkDeleted); + await customersPage.clickBulkDeleteGridRowsButton(); + await customersPage.deleteDialog.clickDeleteButton(); + await customersPage.expectSuccessBanner(); + await expect(customersPage.emptyDataGridListView).toBeVisible(); +}); + +test("TC: SALEOR_206 As an admin I want to add address to the customer and set it as default shipping @e2e @customer", async () => { + await customersPage.gotoCustomerDetailsPage(CUSTOMERS.editCustomer.id); + await addressesListPage.clickManageAddresses(); + await addressesListPage.clickAddAddressButton(); + + const addressUK = ADDRESS.addressUK; + + await addressForm.completeBasicInfoAddressForm(addressUK); + await addressForm.typeCompanyName(addressUK.companyName); + await addressForm.typePhone(addressUK.phone); + await addressForm.typeAddressLine2(addressUK.addressLine2); + await addAddressDialog.clickConfirmButton(); + + const addedAddress = addressesListPage.savedAddress.filter({ + hasText: addressUK.lastName, + }); + const addedAddressCard = addressesListPage.addressCard.filter({ + hasText: addressUK.lastName, + }); + + await expect(addedAddress).toBeVisible(); + await addressesListPage.clickShowMoreMenu(addressUK.lastName); + await addressesListPage.setAsDeafultShippingAddress(); + await expect(addedAddressCard.locator(addressesListPage.addressTypeTitle)).toHaveText( + "Default Shipping Address", + ); +}); + +test("TC: SALEOR_209 As an admin I want to update customer's address and set it as default billing @e2e @customer", async () => { + await customersPage.gotoCustomerDetailsPage(CUSTOMERS.editCustomer.id); + await addressesListPage.clickManageAddresses(); + await addressesListPage.clickShowMoreMenu(CUSTOMERS.editCustomer.initialShippingAddress.lastName); + await addressesListPage.clickEditAddress(); + + const newAddress = ADDRESS.addressUS; + + await addressForm.completeBasicInfoAddressForm(newAddress); + await addressForm.typeCompanyName(newAddress.companyName); + await addressForm.typePhone(newAddress.phone); + await addressForm.typeAddressLine2(newAddress.addressLine2); + await addressForm.selectCountryArea(newAddress.countryArea); + await addAddressDialog.clickConfirmButton(); + await customersPage.expectSuccessBanner(); + await addressesListPage.verifyRequiredAddressFields(newAddress.firstName, newAddress); + await addressesListPage.verifyPhoneField(newAddress.firstName, newAddress); + await addressesListPage.verifyCompanyField(newAddress.firstName, newAddress); + await addressesListPage.verifyAddressLine2Field(newAddress.firstName, newAddress); + + const additionalAddressCard = addressesListPage.addressCard.filter({ + hasText: CUSTOMERS.editCustomer.additionalAddress.lastName, + }); + + await addressesListPage.clickShowMoreMenu(CUSTOMERS.editCustomer.additionalAddress.lastName); + await addressesListPage.setAsDeafultBillingAddress(); + await expect(additionalAddressCard.locator(addressesListPage.addressTypeTitle)).toHaveText( + "Default Billing Address", + ); +}); + +test("TC: SALEOR_210 Delete customer's address @e2e @customer", async () => { + await customersPage.gotoCustomerDetailsPage(CUSTOMERS.editCustomer.id); + await addressesListPage.clickManageAddresses(); + await addressesListPage.clickShowMoreMenu(CUSTOMERS.editCustomer.initialBillingAddress.lastName); + await addressesListPage.clickDeleteAddress(); + await deleteAddressDialog.clickDeleteButton(); + await expect( + addressesListPage.savedAddress.filter({ + hasText: CUSTOMERS.editCustomer.initialBillingAddress.lastName, + }), + ).not.toBeVisible(); +}); + +test("TC: SALEOR_207 Issue a new gift card for the customer @e2e @customer", async () => { + const amount = faker.datatype.number(1000).toPrecision(2).toString(); + + await customersPage.gotoCustomerDetailsPage(CUSTOMERS.editCustomer.id); + await customersPage.clickIssueNewGiftCard(); + await customersPage.issueGiftCardDialog.typeAmount(amount); + await customersPage.issueGiftCardDialog.typeTag(faker.lorem.word()); + await customersPage.issueGiftCardDialog.typeNote(faker.lorem.sentences(3)); + await customersPage.issueGiftCardDialog.clickIssueButton(); + await customersPage.expectSuccessBanner(); + await expect(giftCardsPage.issueGiftCardDialog.cardCode).toBeVisible(); + + const code = (await giftCardsPage.issueGiftCardDialog.cardCode.innerText()).slice(-4); + + await giftCardsPage.issueGiftCardDialog.clickCopyCodeButton(); + await giftCardsPage.expectSuccessBanner(); + await giftCardsPage.issueGiftCardDialog.clickOkButton(); + await giftCardsPage.expectElementIsHidden(giftCardsPage.giftCardDialog); + await giftCardsPage.expectSuccessBannerMessage("Successfully created gift card"); + await giftCardsPage.expectElementIsHidden(giftCardsPage.successBanner); + await giftCardsPage.gotoGiftCardsListView(); + await giftCardsPage.waitForCanvasContainsText(`Code ending with ${code}`); +}); diff --git a/playwright/tests/orders.spec.ts b/playwright/tests/orders.spec.ts index 5118b3daa6f..26d3cb7df9f 100644 --- a/playwright/tests/orders.spec.ts +++ b/playwright/tests/orders.spec.ts @@ -1,5 +1,9 @@ -import { CUSTOMER_ADDRESS, ORDERS, PRODUCTS } from "@data/e2eTestData"; +import { ADDRESS } from "@data/addresses"; +import { ORDERS, PRODUCTS } from "@data/e2eTestData"; +import { AddressesListPage } from "@pages/addressesListPage"; +import { AddressDialog } from "@pages/dialogs/addressDialog"; import { DraftOrdersPage } from "@pages/draftOrdersPage"; +import { AddressForm } from "@pages/forms/addressForm"; import { FulfillmentPage } from "@pages/fulfillmentPage"; import { OrdersPage } from "@pages/ordersPage"; import { expect, test } from "@playwright/test"; @@ -9,11 +13,17 @@ test.use({ storageState: "./playwright/.auth/admin.json" }); let ordersPage: OrdersPage; let draftOrdersPage: DraftOrdersPage; let fulfillmentPage: FulfillmentPage; +let addressDialog: AddressDialog; +let addressForm: AddressForm; +let addressesListPage: AddressesListPage; test.beforeEach(({ page }) => { ordersPage = new OrdersPage(page); draftOrdersPage = new DraftOrdersPage(page); fulfillmentPage = new FulfillmentPage(page); + addressDialog = new AddressDialog(page); + addressesListPage = new AddressesListPage(page); + addressForm = new AddressForm(page); }); const variantSKU = PRODUCTS.productAvailableWithTransactionFlow.variant1sku; @@ -28,18 +38,13 @@ test("TC: SALEOR_28 Create basic order @e2e @order", async () => { await ordersPage.rightSideDetailsPage.clickEditCustomerButton(); await ordersPage.rightSideDetailsPage.clickSearchCustomerInput(); await ordersPage.rightSideDetailsPage.selectCustomer(); - await ordersPage.addressDialog.existingAddressRadioButton.waitFor({ - state: "visible", - timeout: 60000, - }); await ordersPage.addressDialog.clickConfirmButton(); await ordersPage.clickAddShippingCarrierButton(); await ordersPage.shippingAddressDialog.pickAndConfirmFirstShippingMethod(); await ordersPage.clickFinalizeButton(); - await ordersPage.successBanner - .getByText("finalized") - .waitFor({ state: "visible", timeout: 60000 }); + await draftOrdersPage.expectSuccessBannerMessage("finalized"); }); + test("TC: SALEOR_76 Create order with transaction flow activated @e2e @order", async () => { await ordersPage.goToOrdersListView(); await ordersPage.clickCreateOrderButton(); @@ -55,17 +60,13 @@ test("TC: SALEOR_76 Create order with transaction flow activated @e2e @order", a await ordersPage.clickAddShippingCarrierButton(); await ordersPage.shippingAddressDialog.pickAndConfirmFirstShippingMethod(); await ordersPage.clickFinalizeButton(); - await ordersPage.successBanner.getByText("finalized").waitFor({ state: "visible" }); - await expect(ordersPage.markAsPaidButton).toBeVisible(); - await expect(ordersPage.paymentSummarySection).toBeVisible(); - await expect(ordersPage.orderSummarySection).toBeVisible(); - await expect(ordersPage.fulfillButton).toBeDisabled(); + await draftOrdersPage.expectSuccessBannerMessage("finalized"); }); + test("TC: SALEOR_77 Mark order as paid and fulfill it with transaction flow activated @e2e @order", async () => { await ordersPage.goToExistingOrderPage( ORDERS.ordersWithinTransactionFlow.markAsPaidOrder.orderId, ); - await ordersPage.waitForGrid(); await ordersPage.clickMarkAsPaidButton(); await ordersPage.markOrderAsPaidDialog.typeAndSaveOrderReference(); await ordersPage.expectSuccessBannerMessage("paid"); @@ -79,6 +80,7 @@ test("TC: SALEOR_77 Mark order as paid and fulfill it with transaction flow acti await ordersPage.expectSuccessBannerMessage("fulfilled"); expect(await ordersPage.pageHeaderStatusInfo).toContainText("Fulfilled"); }); + test("TC: SALEOR_78 Capture partial amounts by manual transactions and fulfill order with transaction flow activated @e2e @order", async () => { const firstManualTransactionAmount = "100"; const secondManualTransactionAmount = "20"; @@ -86,7 +88,6 @@ test("TC: SALEOR_78 Capture partial amounts by manual transactions and fulfill o await ordersPage.goToExistingOrderPage( ORDERS.ordersWithinTransactionFlow.captureManualTransactionOrder.orderId, ); - await ordersPage.waitForGrid(); await ordersPage.clickManualTransactionButton(); await ordersPage.manualTransactionDialog.completeManualTransactionDialogAndSave( "partial payment 1", @@ -137,9 +138,9 @@ test("TC: SALEOR_78 Capture partial amounts by manual transactions and fulfill o "Fulfilled", ); }); + test("TC: SALEOR_79 Mark order as paid and fulfill it with regular flow @e2e @order", async () => { await ordersPage.goToExistingOrderPage(ORDERS.orderToMarkAsPaidAndFulfill.id); - await ordersPage.waitForGrid(); await ordersPage.clickMarkAsPaidButton(); await ordersPage.markOrderAsPaidDialog.typeAndSaveOrderReference(); await ordersPage.expectSuccessBannerMessage("paid"); @@ -147,52 +148,76 @@ test("TC: SALEOR_79 Mark order as paid and fulfill it with regular flow @e2e @or expect(await ordersPage.paymentStatusInfo, "Order should be fully paid").toContainText( "Fully paid", ); + await ordersPage.clickFulfillButton(); await fulfillmentPage.clickFulfillButton(); await ordersPage.expectSuccessBannerMessage("fulfilled"); expect(await ordersPage.pageHeaderStatusInfo).toContainText("Fulfilled"); }); + test("TC: SALEOR_80 Add tracking to order @e2e @order", async () => { const trackingNumber = "123456789"; await ordersPage.goToExistingOrderPage(ORDERS.orderToAddTrackingNumberTo.id); - await ordersPage.waitForGrid(); await ordersPage.clickAddTrackingButton(); await ordersPage.addTrackingDialog.typeTrackingNumberAndSave(trackingNumber); await ordersPage.expectSuccessBannerMessage("updated"); await expect(ordersPage.setTrackingNumber).toContainText(trackingNumber); }); + test("TC: SALEOR_81 Change billing address in fulfilled order @e2e @order", async () => { await ordersPage.goToExistingOrderPage(ORDERS.orderFulfilledToChangeBillingAddress.id); - await ordersPage.waitForGrid(); await ordersPage.rightSideDetailsPage.clickEditBillingAddressButton(); await ordersPage.addressDialog.clickNewAddressRadioButton(); - await ordersPage.addressDialog.completeAddressFormAllFields( - CUSTOMER_ADDRESS.changeBillingAddress, - ); + + const newAddress = ADDRESS.addressPL; + + await addressForm.completeBasicInfoAddressForm(newAddress); + await addressForm.typeCompanyName(newAddress.companyName); + await addressForm.typePhone(newAddress.phone); + await addressForm.typeAddressLine2(newAddress.addressLine2); + await addressDialog.clickConfirmButton(); + await ordersPage.expectSuccessBanner(); - await ordersPage.expectElementContainsTextFromObjectValues( - ordersPage.rightSideDetailsPage.billingAddressSection, - CUSTOMER_ADDRESS.changeBillingAddress, + + await ordersPage.expectSuccessBanner(); + + await addressesListPage.verifyRequiredAddressFields(newAddress.firstName, newAddress); + await addressesListPage.verifyPhoneField(newAddress.firstName, newAddress); + await addressesListPage.verifyCompanyField(newAddress.firstName, newAddress); + await addressesListPage.verifyAddressLine2Field(newAddress.firstName, newAddress); + await expect(ordersPage.rightSideDetailsPage.billingAddressSection).toContainText( + ADDRESS.addressPL.firstName, + ); + await expect(ordersPage.rightSideDetailsPage.billingAddressSection).toContainText( + ADDRESS.addressPL.firstName, ); }); + test("TC: SALEOR_82 Change shipping address in not fulfilled order @e2e @order", async () => { await ordersPage.goToExistingOrderPage(ORDERS.orderNotFulfilledToChangeShippingAddress.id); - await ordersPage.waitForGrid(); await ordersPage.rightSideDetailsPage.clickEditShippingAddressButton(); await ordersPage.addressDialog.clickNewAddressRadioButton(); - await ordersPage.addressDialog.completeAddressFormAllFields( - CUSTOMER_ADDRESS.changeShippingAddress, - ); + + const newAddress = ADDRESS.addressPL; + + await addressForm.completeBasicInfoAddressForm(newAddress); + await addressForm.typeCompanyName(newAddress.companyName); + await addressForm.typePhone(newAddress.phone); + await addressForm.typeAddressLine2(newAddress.addressLine2); + addressDialog.clickConfirmButton(); await ordersPage.expectSuccessBanner(); - await ordersPage.expectElementContainsTextFromObjectValues( - ordersPage.rightSideDetailsPage.shippingAddressSection, - CUSTOMER_ADDRESS.changeShippingAddress, + await addressesListPage.verifyRequiredAddressFields(newAddress.firstName, newAddress); + await addressesListPage.verifyPhoneField(newAddress.firstName, newAddress); + await addressesListPage.verifyCompanyField(newAddress.firstName, newAddress); + await addressesListPage.verifyAddressLine2Field(newAddress.firstName, newAddress); + await expect(ordersPage.rightSideDetailsPage.shippingAddressSection).toContainText( + ADDRESS.addressPL.firstName, ); }); + test("TC: SALEOR_83 Draft orders bulk delete @e2e @draft", async () => { await draftOrdersPage.goToDraftOrdersListView(); - await draftOrdersPage.waitForGrid(); await draftOrdersPage.checkListRowsBasedOnContainingText(ORDERS.draftOrdersToBeDeleted.ids); await draftOrdersPage.clickBulkDeleteButton(); await draftOrdersPage.deleteDraftOrdersDialog.clickDeleteButton(); @@ -203,41 +228,23 @@ test("TC: SALEOR_83 Draft orders bulk delete @e2e @draft", async () => { `Given draft orders: ${ORDERS.draftOrdersToBeDeleted.ids} should be deleted from the list`, ).toEqual([]); }); + test("TC: SALEOR_84 Create draft order @e2e @draft", async () => { await draftOrdersPage.goToDraftOrdersListView(); - await draftOrdersPage.waitForGrid(); await draftOrdersPage.clickCreateDraftOrderButton(); await draftOrdersPage.draftOrderCreateDialog.completeDraftOrderCreateDialogWithFirstChannel(); await draftOrdersPage.clickAddProductsButton(); - await draftOrdersPage.waitForNetworkIdle(() => - draftOrdersPage.addProductsDialog.searchForProductInDialog( - PRODUCTS.productAvailableWithTransactionFlow.name, - ), + await draftOrdersPage.addProductsDialog.searchForProductInDialog( + PRODUCTS.productAvailableWithTransactionFlow.name, ); - await draftOrdersPage.addProductsDialog.productRow - .filter({ hasText: PRODUCTS.productAvailableWithTransactionFlow.name }) - .waitFor({ state: "visible", timeout: 30000 }); await draftOrdersPage.addProductsDialog.selectVariantBySKU(variantSKU); - await draftOrdersPage.addProductsDialog.waitForDOMToFullyLoad(); - await draftOrdersPage.waitForNetworkIdle(() => - draftOrdersPage.addProductsDialog.clickConfirmButton(), - ); + await draftOrdersPage.addProductsDialog.clickConfirmButton(); await draftOrdersPage.rightSideDetailsPage.clickEditCustomerButton(); await draftOrdersPage.rightSideDetailsPage.clickSearchCustomerInput(); await draftOrdersPage.rightSideDetailsPage.selectCustomer(); - await draftOrdersPage.addressDialog.existingAddressRadioButton.waitFor({ - state: "visible", - timeout: 10000, - }); - await draftOrdersPage.waitForNetworkIdle(() => - draftOrdersPage.addressDialog.clickConfirmButton(), - ); + await draftOrdersPage.addressDialog.clickConfirmButton(); await draftOrdersPage.clickAddShippingCarrierButton(); - await draftOrdersPage.waitForNetworkIdle(() => - draftOrdersPage.shippingAddressDialog.pickAndConfirmFirstShippingMethod(), - ); + await draftOrdersPage.shippingAddressDialog.pickAndConfirmFirstShippingMethod(); await draftOrdersPage.clickFinalizeButton(); - await draftOrdersPage.successBanner - .filter({ hasText: "finalized" }) - .waitFor({ state: "visible", timeout: 60000 }); + await draftOrdersPage.expectSuccessBannerMessage("finalized"); }); diff --git a/playwright/tsconfig.json b/playwright/tsconfig.json index 8ef43d59042..31708474bda 100644 --- a/playwright/tsconfig.json +++ b/playwright/tsconfig.json @@ -13,6 +13,7 @@ "@pages/*": ["./pages/*"], "@api/*": ["./api/*"], "@data/*": ["./data/*"], + "@forms/*": ["./pages/forms/*"], "@dialogs/*": ["./pages/dialogs/*"], "@pageElements/*": ["./pages/pageElements/*"] } diff --git a/src/components/AddressFormatter/AddressFormatter.tsx b/src/components/AddressFormatter/AddressFormatter.tsx index 2c93daca3ed..665c7679716 100644 --- a/src/components/AddressFormatter/AddressFormatter.tsx +++ b/src/components/AddressFormatter/AddressFormatter.tsx @@ -15,26 +15,33 @@ const AddressFormatter: React.FC = ({ address }) => { return (
- + {address.firstName} {address.lastName} - {address.phone} - {address.companyName && {address.companyName}} - + + {address.phone} + + {address.companyName && ( + + {address.companyName} + + )} + {address.streetAddress1}
{address.streetAddress2}
- + {" "} {address.postalCode} {address.city} {address.cityArea ? ", " + address.cityArea : ""} - + {address.countryArea ? address.countryArea + ", " + address.country.country : address.country.country} diff --git a/src/customers/components/CustomerAddress/CustomerAddress.tsx b/src/customers/components/CustomerAddress/CustomerAddress.tsx index 0b5f8421577..0cc8cc82807 100644 --- a/src/customers/components/CustomerAddress/CustomerAddress.tsx +++ b/src/customers/components/CustomerAddress/CustomerAddress.tsx @@ -54,6 +54,7 @@ const messages = defineMessages({ description: "button", }, }); + const useStyles = makeStyles( { actions: { @@ -83,11 +84,13 @@ const CustomerAddress: React.FC = props => { onSetAsDefault, } = props; const classes = useStyles(props); + const intl = useIntl(); return ( - + diff --git a/src/customers/components/CustomerAddressListPage/CustomerAddressListPage.tsx b/src/customers/components/CustomerAddressListPage/CustomerAddressListPage.tsx index 7ef5c99fd28..1a02f263318 100644 --- a/src/customers/components/CustomerAddressListPage/CustomerAddressListPage.tsx +++ b/src/customers/components/CustomerAddressListPage/CustomerAddressListPage.tsx @@ -53,6 +53,7 @@ const messages = defineMessages({ defaultMessage: "There is no address to show for this customer", }, }); + const useStyles = makeStyles( theme => ({ addButton: { @@ -80,10 +81,13 @@ const useStyles = makeStyles( }), { name: "CustomerAddressListPage" }, ); + const CustomerAddressListPage: React.FC = props => { const { customer, disabled, onAdd, onEdit, onRemove, onSetAsDefault } = props; const classes = useStyles(props); + const intl = useIntl(); + const isEmpty = customer?.addresses?.length === 0; const fullName = getStringOrPlaceholder( customer && [customer.firstName, customer.lastName].join(" "), diff --git a/src/customers/components/CustomerCreateDetails/CustomerCreateDetails.tsx b/src/customers/components/CustomerCreateDetails/CustomerCreateDetails.tsx index 34640117e61..913654c20c1 100644 --- a/src/customers/components/CustomerCreateDetails/CustomerCreateDetails.tsx +++ b/src/customers/components/CustomerCreateDetails/CustomerCreateDetails.tsx @@ -32,8 +32,10 @@ export interface CustomerCreateDetailsProps { const CustomerCreateDetails: React.FC = props => { const { data, disabled, errors, onChange } = props; + const classes = useStyles(props); const intl = useIntl(); + const formErrors = getFormErrors(["customerFirstName", "customerLastName", "email"], errors); return ( @@ -48,6 +50,7 @@ const CustomerCreateDetails: React.FC = props => {
= props => { }} /> = props => { }} /> = ({ onChange, }) => { const intl = useIntl(); + const formErrors = getFormErrors(["note"], errors); return ( @@ -44,6 +45,7 @@ const CustomerCreateNote: React.FC = ({ = props => { const { customer, data, disabled, errors, onChange } = props; + const classes = useStyles(props); const intl = useIntl(); + const formErrors = getFormErrors(["note"], errors); return ( @@ -73,6 +75,7 @@ const CustomerDetails: React.FC = props => { /> = props => { onChange={onChange} /> = props => { const { data, disabled, errors, onChange } = props; + const classes = useStyles(props); const intl = useIntl(); + const formErrors = getFormErrors(["firstName", "lastName", "email"], errors); return ( @@ -60,6 +62,7 @@ const CustomerInfo: React.FC = props => { = props => { }} /> = props => { /> = ({ id, params }) => const notify = useNotifier(); const shop = useShop(); const intl = useIntl(); + const [openModal, closeModal] = createDialogActionHandlers< CustomerAddressesUrlDialog, CustomerAddressesUrlQueryParams >(navigate, params => customerAddressesUrl(id, params), params); + const [setCustomerDefaultAddress] = useSetCustomerDefaultAddressMutation({ onCompleted: data => { if (data.addressSetDefault.errors.length === 0) { @@ -50,6 +52,7 @@ const CustomerAddresses: React.FC = ({ id, params }) => } }, }); + const [createCustomerAddress, createCustomerAddressOpts] = useCreateCustomerAddressMutation({ onCompleted: data => { if (data.addressCreate.errors.length === 0) { @@ -57,6 +60,7 @@ const CustomerAddresses: React.FC = ({ id, params }) => } }, }); + const [updateCustomerAddress, updateCustomerAddressOpts] = useUpdateCustomerAddressMutation({ onCompleted: data => { if (data.addressUpdate.errors.length === 0) { @@ -68,6 +72,7 @@ const CustomerAddresses: React.FC = ({ id, params }) => } }, }); + const [removeCustomerAddress, removeCustomerAddressOpts] = useRemoveCustomerAddressMutation({ onCompleted: data => { if (data.addressDelete.errors.length === 0) { @@ -79,12 +84,14 @@ const CustomerAddresses: React.FC = ({ id, params }) => } }, }); + const customerData = useCustomerAddressesQuery({ displayLoader: true, variables: { id, }, }); + const countryChoices = shop?.countries || []; return ( @@ -162,7 +169,7 @@ const CustomerAddresses: React.FC = ({ id, params }) => }) } > - + = ({ id, para const navigate = useNavigator(); const notify = useNotifier(); const intl = useIntl(); + const customerDetails = useCustomerDetails(); const user = customerDetails?.customer?.user; const customerDetailsLoading = customerDetails?.loading; + const [removeCustomer, removeCustomerOpts] = useRemoveCustomerMutation({ onCompleted: data => { if (data.customerDelete.errors.length === 0) { @@ -50,6 +52,7 @@ const CustomerDetailsViewInner: React.FC = ({ id, para } }, }); + const [updateCustomer, updateCustomerOpts] = useUpdateCustomerMutation({ onCompleted: data => { if (data.customerUpdate.errors.length === 0) { @@ -60,6 +63,7 @@ const CustomerDetailsViewInner: React.FC = ({ id, para } }, }); + const [updateMetadata] = useUpdateMetadataMutation({}); const [updatePrivateMetadata] = useUpdatePrivateMetadataMutation({}); @@ -82,6 +86,7 @@ const CustomerDetailsViewInner: React.FC = ({ id, para }, }), ); + const handleSubmit = createMetadataUpdateHandler( user, updateData, @@ -91,7 +96,7 @@ const CustomerDetailsViewInner: React.FC = ({ id, para return ( <> - + { const customerDetails = useCustomerDetails(); const customer = customerDetails?.customer?.user; const id = customer?.id; + const { data, loading } = useCustomerGiftCardListQuery({ variables: { first: 5, @@ -38,14 +39,19 @@ const CustomerGiftCardsCard: React.FC = () => { }, skip: !id, }); + const closeCreateDialog = () => setOpenCreateDialog(false); + const giftCards = mapEdgesToItems(data?.giftCards); + const classes = useCardActionsStyles({ buttonPosition: giftCards?.length > 0 ? "right" : "left", }); + const viewAllGiftCardsUrl = giftCardListUrl({ usedBy: [id], }); + const handleCreateNewCardButton = () => { setOpenCreateDialog(true); }; @@ -85,7 +91,11 @@ const CustomerGiftCardsCard: React.FC = () => { )} -