diff --git a/e2e/fixtures/account/addresses-page.ts b/e2e/fixtures/account/addresses-page.ts index eb6493d5b..3a0734cf3 100644 --- a/e2e/fixtures/account/addresses-page.ts +++ b/e2e/fixtures/account/addresses-page.ts @@ -1,12 +1,21 @@ import { Locator, Page } from "@playwright/test" import { AccountPage } from "./account-page" -import { AddAddressModal } from "./modals/add-address-modal" +import { AddressModal } from "./modals/address-modal" export class AddressesPage extends AccountPage { - addAddressModal: AddAddressModal + addAddressModal: AddressModal + editAddressModal: AddressModal + addressContainer: Locator + addressesWrapper: Locator + newAddressButton: Locator + constructor(page: Page) { super(page) - this.addAddressModal = new AddAddressModal(page) + this.addAddressModal = new AddressModal(page, "add") + this.editAddressModal = new AddressModal(page, "edit") + this.addressContainer = this.container.getByTestId("address-container") + this.addressesWrapper = page.getByTestId("addresses-page-wrapper") + this.newAddressButton = this.container.getByTestId("add-address-button") } getAddressContainer(text: string) { @@ -15,7 +24,7 @@ export class AddressesPage extends AccountPage { .filter({ hasText: text }) return { container, - editButton: container.getByTestId("address-edit-button"), + editButton: container.getByTestId('address-edit-button'), deleteButton: container.getByTestId("address-delete-button"), name: container.getByTestId("address-name"), company: container.getByTestId("address-company"), @@ -24,4 +33,10 @@ export class AddressesPage extends AccountPage { provinceCountry: container.getByTestId("address-province-country"), } } + + async goto() { + await super.goto() + await this.addressesLink.click() + await this.addressesWrapper.waitFor({ state: "visible" }) + } } diff --git a/e2e/fixtures/account/modals/add-address-modal.ts b/e2e/fixtures/account/modals/address-modal.ts similarity index 76% rename from e2e/fixtures/account/modals/add-address-modal.ts rename to e2e/fixtures/account/modals/address-modal.ts index f1f05244c..6917bf1bd 100644 --- a/e2e/fixtures/account/modals/add-address-modal.ts +++ b/e2e/fixtures/account/modals/address-modal.ts @@ -1,8 +1,8 @@ import { Page, Locator } from "@playwright/test" import { BaseModal } from "../../base/base-modal" -export class AddAddressModal extends BaseModal { - addAddressButton: Locator +export class AddressModal extends BaseModal { + saveButton: Locator cancelButton: Locator firstNameInput: Locator @@ -16,10 +16,14 @@ export class AddAddressModal extends BaseModal { countrySelect: Locator phoneInput: Locator - constructor(page: Page) { - super(page, page.getByTestId("add-address-modal")) + constructor(page: Page, modalType: "add" | "edit") { + if (modalType === "add") { + super(page, page.getByTestId("add-address-modal")) + } else { + super(page, page.getByTestId("edit-address-modal")) + } - this.addAddressButton = this.container.getByTestId("add-address-button") + this.saveButton = this.container.getByTestId("save-button") this.cancelButton = this.container.getByTestId("cancel-button") this.firstNameInput = this.container.getByTestId("first-name-input") diff --git a/e2e/fixtures/base/nav-menu.ts b/e2e/fixtures/base/nav-menu.ts index 78149ef19..d17af5bc2 100644 --- a/e2e/fixtures/base/nav-menu.ts +++ b/e2e/fixtures/base/nav-menu.ts @@ -23,7 +23,7 @@ export class NavMenu { this.storeLink = this.navMenu.getByTestId("store-link") this.searchLink = this.navMenu.getByTestId("search-link") this.accountLink = this.navMenu.getByTestId("account-link") - this.cartLink = this.navMenu.getByTestId("cart-link") + this.cartLink = this.navMenu.getByTestId("nav-cart-link") this.closeButton = this.navMenu.getByTestId("close-menu-button") this.shippingToLink = this.navMenu.getByTestId("shipping-to-button") this.shippingToMenu = this.navMenu.getByTestId("shipping-to-choices") diff --git a/e2e/fixtures/category-page.ts b/e2e/fixtures/category-page.ts index ebcb35675..6ca67cb41 100644 --- a/e2e/fixtures/category-page.ts +++ b/e2e/fixtures/category-page.ts @@ -5,6 +5,7 @@ export class CategoryPage extends BasePage { container: Locator sortByContainer: Locator + pageTitle: Locator pagination: Locator productsListLoader: Locator productsList: Locator @@ -13,6 +14,7 @@ export class CategoryPage extends BasePage { constructor(page: Page) { super(page) this.container = page.getByTestId("category-container") + this.pageTitle = page.getByTestId("category-page-title") this.sortByContainer = page.getByTestId("sort-by-container") this.productsListLoader = this.container.getByTestId("products-list-loader") this.productsList = this.container.getByTestId("products-list") @@ -21,14 +23,9 @@ export class CategoryPage extends BasePage { } async getProduct(name: string) { - const productTitle = await this.container - .getByTestId("product-title") - .filter({ - hasText: name, - }) - const product = this.productWrapper.filter({ has: productTitle }) + const product = this.productWrapper.filter({ hasText: name }) return { - product, + locator: product, title: product.getByTestId("product-title"), price: product.getByTestId("price"), originalPrice: product.getByTestId("original-price"), diff --git a/e2e/fixtures/checkout-page.ts b/e2e/fixtures/checkout-page.ts index 9dc9666bf..1f670d122 100644 --- a/e2e/fixtures/checkout-page.ts +++ b/e2e/fixtures/checkout-page.ts @@ -21,9 +21,9 @@ export class CheckoutPage extends BasePage { billingPhoneInput: Locator billingPostalInput: Locator billingProvinceInput: Locator - shippingAddress2Input: Locator shippingAddressInput: Locator shippingCityInput: Locator + shippingCompanyInput: Locator shippingFirstNameInput: Locator shippingLastNameInput: Locator shippingPhoneInput: Locator @@ -114,13 +114,11 @@ export class CheckoutPage extends BasePage { this.billingProvinceInput = this.container.getByTestId( "billing-province-input" ) - this.shippingAddress2Input = this.container.getByTestId( - "shipping-address-2-input" - ) this.shippingAddressInput = this.container.getByTestId( "shipping-address-input" ) this.shippingCityInput = this.container.getByTestId("shipping-city-input") + this.shippingCompanyInput = this.container.getByTestId("shipping-company-input") this.shippingFirstNameInput = this.container.getByTestId( "shipping-first-name-input" ) @@ -237,15 +235,15 @@ export class CheckoutPage extends BasePage { hasText: address, }) await addressOption.getByTestId("shipping-address-radio").click() - const addressText = (await addressOption.getAttribute("value")) || "" + const selectHandle = await this.shippingAddressSelect.elementHandle() await this.page.waitForFunction( (opts) => { const select = opts[0] const choice = opts[1] - return select.textContent === choice + return (select.textContent||"").includes(choice) }, - [selectHandle, addressText] as [ElementHandle, string] + [selectHandle, address] as [ElementHandle, string] ) } diff --git a/e2e/fixtures/index.ts b/e2e/fixtures/index.ts index b85536f34..ff87f33e9 100644 --- a/e2e/fixtures/index.ts +++ b/e2e/fixtures/index.ts @@ -5,6 +5,7 @@ import { CategoryPage } from "./category-page" import { CheckoutPage } from "./checkout-page" import { OrderPage } from "./order-page" import { ProductPage } from "./product-page" +import { StorePage } from "./store-page" export const fixtures = base.extend<{ resetDatabaseFixture: void @@ -13,6 +14,7 @@ export const fixtures = base.extend<{ checkoutPage: CheckoutPage orderPage: OrderPage productPage: ProductPage + storePage: StorePage }>({ page: async ({ page }, use) => { await page.goto("/") @@ -45,4 +47,8 @@ export const fixtures = base.extend<{ const productPage = new ProductPage(page) await use(productPage) }, + storePage: async ({ page }, use) => { + const storePage = new StorePage(page) + await use(storePage) + }, }) diff --git a/e2e/fixtures/store-page.ts b/e2e/fixtures/store-page.ts new file mode 100644 index 000000000..e69864c29 --- /dev/null +++ b/e2e/fixtures/store-page.ts @@ -0,0 +1,18 @@ +import { Locator, Page } from "@playwright/test" +import { CategoryPage } from "./category-page" + +export class StorePage extends CategoryPage { + pageTitle: Locator + + constructor(page: Page) { + super(page) + this.pageTitle = page.getByTestId("store-page-title") + } + + async goto() { + await this.navMenu.open() + await this.navMenu.storeLink.click() + await this.pageTitle.waitFor({ state: "visible" }) + await this.productsListLoader.waitFor({ state: "hidden" }) + } +} \ No newline at end of file diff --git a/e2e/tests/authenticated/address.spec.ts b/e2e/tests/authenticated/address.spec.ts new file mode 100644 index 000000000..c19ecca00 --- /dev/null +++ b/e2e/tests/authenticated/address.spec.ts @@ -0,0 +1,258 @@ +import { AddressesPage } from "../../fixtures/account/addresses-page" +import { test, expect } from "../../index" +import { getSelectedOptionText } from "../../utils/locators" + +test.describe("Addresses tests", () => { + test("Creating a new address is displayed during checkout", async ({ + accountAddressesPage: addressesPage, + cartPage, + checkoutPage, + productPage, + storePage, + }) => { + await test.step("Navigate to the new address modal", async () => { + await addressesPage.goto() + await addressesPage.newAddressButton.click() + await addressesPage.addAddressModal.container.waitFor({ state: "visible" }) + }) + + await test.step("Inputs and saves the new address", async () => { + const modal = addressesPage.addAddressModal + await modal.firstNameInput.fill("First") + await modal.lastNameInput.fill("Last") + await modal.companyInput.fill("FirstCorp") + await modal.address1Input.fill("123 Fake Street") + await modal.address2Input.fill("Apt 1") + await modal.postalCodeInput.fill("11111") + await modal.cityInput.fill("City") + await modal.stateInput.fill("Colorado") + await modal.countrySelect.selectOption({ + label: "United States", + }) + await modal.phoneInput.fill("1112223333") + await modal.saveButton.click() + await modal.container.waitFor({ state: "hidden" }) + }) + + await test.step("Navigate to a product page and add a product to the cart", async () => { + await storePage.goto() + const product = await storePage.getProduct("Sweatshirt") + await product.locator.highlight() + await product.locator.click() + await productPage.container.waitFor({ state: "visible" }) + await productPage.selectOption("M") + await productPage.addProductButton.click() + await productPage.cartDropdown.navCartLink.click() + await productPage.cartDropdown.goToCartButton.click() + await cartPage.container.waitFor({ state: "visible" }) + await cartPage.checkoutButton.click() + await checkoutPage.container.waitFor({ state: "visible" }) + }) + + await test.step("Verify the address is correct in the checkout process", async () => { + await checkoutPage.selectSavedAddress("123 Fake Street") + await expect(checkoutPage.shippingFirstNameInput).toHaveValue("First") + await expect(checkoutPage.shippingLastNameInput).toHaveValue("Last") + await expect(checkoutPage.shippingCompanyInput).toHaveValue("FirstCorp") + await expect(checkoutPage.shippingAddressInput).toHaveValue( + "123 Fake Street" + ) + await expect(checkoutPage.shippingPostalCodeInput).toHaveValue("11111") + await expect(checkoutPage.shippingCityInput).toHaveValue("City") + await expect(checkoutPage.shippingProvinceInput).toHaveValue("Colorado") + expect( + await getSelectedOptionText( + checkoutPage.page, + checkoutPage.shippingCountrySelect + ) + ).toContain("United States") + }) + }) + + test("Performing all the CRUD actions for an address", async ({ + accountAddressesPage: addressesPage, + }) => { + await test.step("Navigate to the new address modal", async () => { + await addressesPage.goto() + await addressesPage.newAddressButton.click() + await addressesPage.addAddressModal.container.waitFor({ state: "visible" }) + }) + + await test.step("Input and save a new address", async () => { + const { addAddressModal } = addressesPage + await addAddressModal.firstNameInput.fill("First") + await addAddressModal.lastNameInput.fill("Last") + await addAddressModal.companyInput.fill("MyCorp") + await addAddressModal.address1Input.fill("123 Fake Street") + await addAddressModal.address2Input.fill("Apt 1") + await addAddressModal.postalCodeInput.fill("80010") + await addAddressModal.cityInput.fill("Denver") + await addAddressModal.stateInput.fill("Colorado") + await addAddressModal.countrySelect.selectOption({ label: "United States" }) + await addAddressModal.phoneInput.fill("3031112222") + await addAddressModal.saveButton.click() + await addAddressModal.container.waitFor({ state: "hidden" }) + }) + + let addressContainer: ReturnType + await test.step("Make sure the address container was appended to the page", async () => { + addressContainer = addressesPage.getAddressContainer("First Last") + await expect(addressContainer.name).toHaveText("First Last") + await expect(addressContainer.company).toHaveText("MyCorp") + await expect(addressContainer.address).toContainText("123 Fake Street") + await expect(addressContainer.address).toContainText("Apt 1") + await expect(addressContainer.postalCity).toContainText("80010, Denver") + await expect(addressContainer.provinceCountry).toContainText("Colorado, US") + }) + + await test.step("Refresh the page and assert address was saved", async () => { + await addressesPage.page.reload() + addressContainer = addressesPage.getAddressContainer("First Last") + await expect(addressContainer.name).toHaveText("First Last") + await expect(addressContainer.company).toHaveText("MyCorp") + await expect(addressContainer.address).toContainText("123 Fake Street") + await expect(addressContainer.address).toContainText("Apt 1") + await expect(addressContainer.postalCity).toContainText("80010, Denver") + await expect(addressContainer.provinceCountry).toContainText("Colorado, US") + }) + + await test.step("Edit the address", async () => { + await addressContainer.editButton.click() + await addressesPage.editAddressModal.container.waitFor({ state: "visible" }) + await addressesPage.editAddressModal.firstNameInput.fill("Second") + await addressesPage.editAddressModal.lastNameInput.fill("Final") + await addressesPage.editAddressModal.companyInput.fill("MeCorp") + await addressesPage.editAddressModal.address1Input.fill("123 Spark Street") + await addressesPage.editAddressModal.address2Input.fill("Unit 3") + await addressesPage.editAddressModal.postalCodeInput.fill("80011") + await addressesPage.editAddressModal.cityInput.fill("Broomfield") + await addressesPage.editAddressModal.stateInput.fill("CO") + await addressesPage.editAddressModal.countrySelect.selectOption({ + label: "Canada", + }) + await addressesPage.editAddressModal.phoneInput.fill("3032223333") + await addressesPage.editAddressModal.saveButton.click() + await addressesPage.editAddressModal.container.waitFor({ state: "hidden" }) + }) + + await test.step("Make sure edits were saved on the addressContainer", async () => { + addressContainer = addressesPage.getAddressContainer("Second Final") + await expect(addressContainer.name).toContainText("Second Final") + await expect(addressContainer.company).toContainText("MeCorp") + await expect(addressContainer.address).toContainText("123 Spark Street, Unit 3") + await expect(addressContainer.postalCity).toContainText("80011, Broomfield") + await expect(addressContainer.provinceCountry).toContainText("CO, CA") + }) + + await test.step("Refresh the page and assert edits were saved", async () => { + await addressesPage.page.reload() + await expect(addressContainer.name).toContainText("Second Final") + await expect(addressContainer.company).toContainText("MeCorp") + await expect(addressContainer.address).toContainText("123 Spark Street, Unit 3") + await expect(addressContainer.postalCity).toContainText("80011, Broomfield") + await expect(addressContainer.provinceCountry).toContainText("CO, CA") + }) + + await test.step("Delete the address", async () => { + await addressContainer.deleteButton.click() + await addressContainer.container.waitFor({ state: "hidden" }) + await addressesPage.page.reload() + await expect(addressContainer.container).not.toBeVisible() + }) + + await test.step("Ensure address remains deleted after refresh", async () => { + await addressesPage.page.reload() + await expect(addressContainer.container).not.toBeVisible() + }) + }) + + test.skip("Attempt to create duplicate addresses on the address page", async ({ + accountAddressesPage: addressesPage + }) => { + await test.step("navigate to the new address modal", async () => { + await addressesPage.goto() + await addressesPage.newAddressButton.click() + await addressesPage.addAddressModal.container.waitFor({ state: "visible" }) + }) + + await test.step("Input and save a new address", async () => { + await addressesPage.addAddressModal.firstNameInput.fill("First") + await addressesPage.addAddressModal.lastNameInput.fill("Last") + await addressesPage.addAddressModal.companyInput.fill("MyCorp") + await addressesPage.addAddressModal.address1Input.fill("123 Fake Street") + await addressesPage.addAddressModal.address2Input.fill("Apt 1") + await addressesPage.addAddressModal.postalCodeInput.fill("80010") + await addressesPage.addAddressModal.cityInput.fill("Denver") + await addressesPage.addAddressModal.stateInput.fill("Colorado") + await addressesPage.addAddressModal.countrySelect.selectOption({ + label: "United States", + }) + await addressesPage.addAddressModal.phoneInput.fill("3031112222") + await addressesPage.addAddressModal.saveButton.click() + await addressesPage.addAddressModal.container.waitFor({ state: "hidden" }) + }) + + await test.step("Attempt to create the same address", async () => { + await addressesPage.newAddressButton.click() + await addressesPage.addAddressModal.container.waitFor({ state: "visible" }) + await addressesPage.addAddressModal.firstNameInput.fill("First") + await addressesPage.addAddressModal.lastNameInput.fill("Last") + await addressesPage.addAddressModal.companyInput.fill("MyCorp") + await addressesPage.addAddressModal.address1Input.fill("123 Fake Street") + await addressesPage.addAddressModal.address2Input.fill("Apt 1") + await addressesPage.addAddressModal.postalCodeInput.fill("80010") + await addressesPage.addAddressModal.cityInput.fill("Denver") + await addressesPage.addAddressModal.stateInput.fill("Colorado") + await addressesPage.addAddressModal.countrySelect.selectOption({ + label: "United States", + }) + await addressesPage.addAddressModal.phoneInput.fill("3031112222") + await addressesPage.addAddressModal.saveButton.click() + }) + + await test.step("Validate error state", async () => { + + }) + }) + + test("Creating multiple tests works correctly", async ({ + accountAddressesPage: addressesPage, + }) => { + test.slow() + await test.step("Navigate to the new address modal", async () => { + await addressesPage.goto() + }) + + let addressContainer: ReturnType + for (let i = 0; i < 10; i++) { + await test.step("Open up the new address modal", async () => { + await addressesPage.newAddressButton.click() + await addressesPage.addAddressModal.container.waitFor({ state: "visible" }) + }) + await test.step("Input and save a new address", async () => { + const { addAddressModal } = addressesPage + await addAddressModal.firstNameInput.fill(`First-${i}`) + await addAddressModal.lastNameInput.fill(`Last-${i}`) + await addAddressModal.companyInput.fill(`MyCorp-${i}`) + await addAddressModal.address1Input.fill(`123 Fake Street-${i}`) + await addAddressModal.address2Input.fill("Apt 1") + await addAddressModal.postalCodeInput.fill("80010") + await addAddressModal.cityInput.fill("Denver") + await addAddressModal.stateInput.fill("Colorado") + await addAddressModal.countrySelect.selectOption({ label: "United States" }) + await addAddressModal.phoneInput.fill("3031112222") + await addAddressModal.saveButton.click() + await addAddressModal.container.waitFor({ state: "hidden" }) + }) + await test.step("Make sure the address container was appended to the page", async () => { + addressContainer = addressesPage.getAddressContainer(`First-${i} Last-${i}`) + await expect(addressContainer.name).toHaveText(`First-${i} Last-${i}`) + await expect(addressContainer.company).toHaveText(`MyCorp-${i}`) + await expect(addressContainer.address).toContainText(`123 Fake Street-${i}`) + await expect(addressContainer.address).toContainText("Apt 1") + await expect(addressContainer.postalCity).toContainText("80010, Denver") + await expect(addressContainer.provinceCountry).toContainText("Colorado, US") + }) + } + }) +}) \ No newline at end of file diff --git a/e2e/utils/locators.ts b/e2e/utils/locators.ts new file mode 100644 index 000000000..f87c936b2 --- /dev/null +++ b/e2e/utils/locators.ts @@ -0,0 +1,13 @@ +import { Page, Locator} from '@playwright/test' + +export async function getSelectedOptionText(page: Page, select: Locator) { + const handle = await select.elementHandle() + return await page.evaluate( + (opts) => { + if (!opts || !opts[0]) { return "" } + const select = opts[0] as HTMLSelectElement + return select.options[select.selectedIndex].textContent + }, + [handle] + ) +} diff --git a/src/app/[countryCode]/(main)/account/@dashboard/addresses/page.tsx b/src/app/[countryCode]/(main)/account/@dashboard/addresses/page.tsx index 61862d6eb..e3125729d 100644 --- a/src/app/[countryCode]/(main)/account/@dashboard/addresses/page.tsx +++ b/src/app/[countryCode]/(main)/account/@dashboard/addresses/page.tsx @@ -23,7 +23,7 @@ export default async function Addresses() { } return ( -
+

Shipping Addresses

diff --git a/src/modules/account/components/address-card/add-address.tsx b/src/modules/account/components/address-card/add-address.tsx index 16d64b581..b3c961e4c 100644 --- a/src/modules/account/components/address-card/add-address.tsx +++ b/src/modules/account/components/address-card/add-address.tsx @@ -120,6 +120,7 @@ const AddAddress = ({ region }: { region: Region }) => { name="country_code" required autoComplete="country" + data-testid="country-select" />

diff --git a/src/modules/account/components/address-card/edit-address-modal.tsx b/src/modules/account/components/address-card/edit-address-modal.tsx index fb72a0ea7..29a983eb8 100644 --- a/src/modules/account/components/address-card/edit-address-modal.tsx +++ b/src/modules/account/components/address-card/edit-address-modal.tsx @@ -71,8 +71,9 @@ const EditAddress: React.FC = ({ "border-gray-900": isActive, } )} + data-testid="address-container" > -
+
{address.first_name} {address.last_name} @@ -115,7 +116,7 @@ const EditAddress: React.FC = ({
- + Edit address @@ -129,6 +130,7 @@ const EditAddress: React.FC = ({ required autoComplete="given-name" defaultValue={address.first_name || undefined} + data-testid="first-name-input" /> = ({ required autoComplete="family-name" defaultValue={address.last_name || undefined} + data-testid="last-name-input" />
= ({ name="company" autoComplete="organization" defaultValue={address.company || undefined} + data-testid="company-input" /> = ({ required autoComplete="address-line1" defaultValue={address.address_1 || undefined} + data-testid="address-1-input" />
= ({ required autoComplete="postal-code" defaultValue={address.postal_code || undefined} + data-testid="postal-code-input" /> = ({ required autoComplete="locality" defaultValue={address.city || undefined} + data-testid="city-input" />
= ({ name="province" autoComplete="address-level1" defaultValue={address.province || undefined} + data-testid="state-input" /> = ({ required autoComplete="country" defaultValue={address.country_code || undefined} + data-testid="country-select" />
{formState.error && ( @@ -206,10 +217,11 @@ const EditAddress: React.FC = ({ variant="secondary" onClick={close} className="h-10" + data-testid="cancel-button" > Cancel - Save + Save diff --git a/src/modules/categories/templates/index.tsx b/src/modules/categories/templates/index.tsx index 160761237..64b64d226 100644 --- a/src/modules/categories/templates/index.tsx +++ b/src/modules/categories/templates/index.tsx @@ -45,7 +45,7 @@ export default function CategoryTemplate({ / ))} -

{category.name}

+

{category.name}

{category.description && (
diff --git a/src/modules/checkout/components/address-select/index.tsx b/src/modules/checkout/components/address-select/index.tsx index 5cdbdbd3f..270302932 100644 --- a/src/modules/checkout/components/address-select/index.tsx +++ b/src/modules/checkout/components/address-select/index.tsx @@ -65,14 +65,14 @@ const AddressSelect = ({ addresses, cart }: AddressSelectProps) => { > + data-testid="shipping-address-options"> {addresses.map((address) => { return (
diff --git a/src/modules/checkout/components/shipping-address/index.tsx b/src/modules/checkout/components/shipping-address/index.tsx index af8f31ac1..63d966b39 100644 --- a/src/modules/checkout/components/shipping-address/index.tsx +++ b/src/modules/checkout/components/shipping-address/index.tsx @@ -118,7 +118,7 @@ const ShippingAddress = ({ value={formData["shipping_address.company"]} onChange={handleChange} autoComplete="organization" - data-testid="shipping-address-2-input" + data-testid="shipping-company-input" /> {`Cart (${totalItems})`} +
-

All products

+

All products

}>