diff --git a/e2e/fixtures/base/cart-dropdown.ts b/e2e/fixtures/base/cart-dropdown.ts index bdd734bb0..40a1440bb 100644 --- a/e2e/fixtures/base/cart-dropdown.ts +++ b/e2e/fixtures/base/cart-dropdown.ts @@ -19,23 +19,33 @@ export class CartDropdown { await this.navCartLink.hover() } - async getCartItem(name: string) { - const cartItem = this.cartDropdown.getByTestId("cart-item").filter({ - hasText: name, - }) - const quantity = cartItem - .getByTestId("cart-item-quantity") - .getAttribute("data-value") - const variant = cartItem - .getByTestId("cart-item-variant") - .getAttribute("data-value") + async close() { + if (await this.cartDropdown.isVisible()) { + const box = await this.cartDropdown.boundingBox() + if (!box) { + return + } + await this.page.mouse.move(box.x + box.width / 4, box.y + box.height / 4) + await this.page.mouse.move(5, 10) + } + } + + async getCartItem(name: string, variant: string) { + const cartItem = this.cartDropdown + .getByTestId("cart-item") + .filter({ + hasText: name, + }) + .filter({ + hasText: `Variant: ${variant}`, + }) return { locator: cartItem, productLink: cartItem.getByTestId("product-link"), removeButton: cartItem.getByTestId("cart-item-remove-button"), name, - quantity, - variant, + quantity: cartItem.getByTestId("cart-item-quantity"), + variant: cartItem.getByTestId("cart-item-variant"), } } } diff --git a/e2e/fixtures/cart-page.ts b/e2e/fixtures/cart-page.ts index 6655cbad7..138305040 100644 --- a/e2e/fixtures/cart-page.ts +++ b/e2e/fixtures/cart-page.ts @@ -23,6 +23,7 @@ export class CartPage extends BasePage { cartGiftCardAmount: Locator cartShipping: Locator cartTaxes: Locator + cartTotal: Locator checkoutButton: Locator constructor(page: Page) { @@ -61,22 +62,23 @@ export class CartPage extends BasePage { ) this.cartShipping = this.container.getByTestId("cart-shipping") this.cartTaxes = this.container.getByTestId("cart-taxes") + this.cartTotal = this.container.getByTestId("cart-total") } async getProduct(title: string, variant: string) { const productRow = this.productRow .filter({ - has: this.productTitle.filter({ hasText: title }), + hasText: title, }) .filter({ - has: this.productVariant.filter({ hasText: variant }), + hasText: `Variant: ${variant}`, }) return { productRow, title: productRow.getByTestId("product-title"), variant: productRow.getByTestId("product-variant"), deleteButton: productRow.getByTestId("delete-button"), - quantitySelect: productRow.getByTestId("quantity-select"), + quantitySelect: productRow.getByTestId("product-select-button"), price: productRow.getByTestId("product-unit-price"), total: productRow.getByTestId("product-price"), } diff --git a/e2e/fixtures/product-page.ts b/e2e/fixtures/product-page.ts index c3c7bb45b..66b79ba9e 100644 --- a/e2e/fixtures/product-page.ts +++ b/e2e/fixtures/product-page.ts @@ -9,6 +9,7 @@ export class ProductPage extends BasePage { productTitle: Locator productDescription: Locator productOptions: Locator + productPrice: Locator addProductButton: Locator mobileActionsContainer: Locator mobileTitle: Locator @@ -24,6 +25,7 @@ export class ProductPage extends BasePage { this.productTitle = this.container.getByTestId("product-title") this.productDescription = this.container.getByTestId("product-description") this.productOptions = this.container.getByTestId("product-options") + this.productPrice = this.container.getByTestId("product-price") this.addProductButton = this.container.getByTestId("add-product-button") this.mobileActionsContainer = page.getByTestId("mobile-actions") this.mobileTitle = this.mobileActionsContainer.getByTestId("mobile-title") @@ -35,10 +37,16 @@ export class ProductPage extends BasePage { ) } + async clickAddProduct() { + await this.addProductButton.click() + await this.cartDropdown.cartDropdown.waitFor({ state: "visible" }) + } + async selectOption(option: string) { + await this.page.mouse.move(0, 0) // hides the checkout container const optionButton = this.productOptions .getByTestId("option-button") .filter({ hasText: option }) - await optionButton.click() + await optionButton.click({ clickCount: 2 }) } } diff --git a/e2e/tests/public/cart.spec.ts b/e2e/tests/public/cart.spec.ts new file mode 100644 index 000000000..edaaaf666 --- /dev/null +++ b/e2e/tests/public/cart.spec.ts @@ -0,0 +1,202 @@ +/* +Test List +- login from the sign in page redirects you page to the cart +*/ +import { test, expect } from "../../index" +import { compareFloats, getFloatValue } from "../../utils" + +test.describe("Cart tests", async () => { + test("Ensure adding multiple items from a product page adjusts the cart accordingly", async ({ + page, + cartPage, + productPage, + storePage, + }) => { + // Assuming we have access to our page objects here + const cartDropdown = cartPage.cartDropdown + + await test.step("Navigate to the product page", async () => { + await storePage.goto() + const product = await storePage.getProduct("Sweatshirt") + await product.locator.click() + await productPage.container.waitFor({ state: "visible" }) + }) + + await test.step("Add the small size to the cart and verify the data", async () => { + await productPage.selectOption("S") + await productPage.addProductButton.click() + await expect(cartDropdown.navCartLink).toContainText("(1)") + const cartItem = await cartDropdown.getCartItem("Sweatshirt", "S") + await expect(cartItem.locator).toBeVisible() + await expect(cartItem.variant).toContainText("S") + await expect(cartItem.quantity).toContainText("1") + await cartDropdown.goToCartButton.click() + await cartDropdown.close() + await cartPage.container.waitFor({ state: "visible" }) + const productInCart = await cartPage.getProduct("Sweatshirt", "S") + await expect(productInCart.productRow).toBeVisible() + await expect(productInCart.quantitySelect).toHaveValue("1") + await page.goBack() + await productPage.container.waitFor({ state: "visible" }) + }) + + await test.step("Add the small size to the cart again and verify the data", async () => { + await productPage.selectOption("S") + await productPage.addProductButton.click() + await expect(cartDropdown.navCartLink).toContainText("(2)") + const cartItem = await cartDropdown.getCartItem("Sweatshirt", "S") + await expect(cartItem.locator).toBeVisible() + await expect(cartItem.variant).toContainText("S") + await expect(cartItem.quantity).toContainText("2") + await cartDropdown.goToCartButton.click() + await cartDropdown.close() + await cartPage.container.waitFor({ state: "visible" }) + const productInCart = await cartPage.getProduct("Sweatshirt", "S") + await expect(productInCart.productRow).toBeVisible() + await expect(productInCart.quantitySelect).toHaveValue("2") + await page.goBack() + await productPage.container.waitFor({ state: "visible" }) + }) + + await test.step("Add the medium size to the cart and verify the data", async () => { + await productPage.selectOption("M") + await productPage.addProductButton.click() + await expect(cartDropdown.navCartLink).toContainText("(3)") + const mediumCartItem = await cartDropdown.getCartItem("Sweatshirt", "M") + await expect(mediumCartItem.locator).toBeVisible() + await expect(mediumCartItem.variant).toContainText("M") + await expect(mediumCartItem.quantity).toContainText("1") + await cartDropdown.goToCartButton.click() + await cartDropdown.close() + await cartPage.container.waitFor({ state: "visible" }) + const mediumProductInCart = await cartPage.getProduct("Sweatshirt", "M") + await expect(mediumProductInCart.productRow).toBeVisible() + await expect(mediumProductInCart.quantitySelect).toHaveValue("1") + const smallProductInCart = await cartPage.getProduct("Sweatshirt", "S") + await expect(smallProductInCart.productRow).toBeVisible() + await expect(smallProductInCart.quantitySelect).toHaveValue("2") + }) + }) + + test("Ensure adding two products into the cart and verify the quantities", async ({ + cartPage, + productPage, + storePage, + }) => { + const cartDropdown = cartPage.cartDropdown + + await test.step("Navigate to the product page - go to the store page and click on the Sweatshirt product", async () => { + await storePage.goto() + const product = await storePage.getProduct("Sweatshirt") + await product.locator.click() + await productPage.container.waitFor({ state: "visible" }) + }) + + await test.step("Add the small sweatshirt to the cart", async () => { + await productPage.selectOption("S") + await productPage.addProductButton.click() + await expect(cartDropdown.navCartLink).toContainText("(1)") + const sweatshirtItem = await cartDropdown.getCartItem("Sweatshirt", "S") + await expect(sweatshirtItem.locator).toBeVisible() + await expect(sweatshirtItem.variant).toHaveText("Variant: S") + await expect(sweatshirtItem.quantity).toContainText("1") + await cartDropdown.close() + }) + + await test.step("Navigate to another product - Sweatpants", async () => { + await storePage.goto() + const product = await storePage.getProduct("Sweatpants") + await product.locator.click() + await productPage.container.waitFor({ state: "visible" }) + }) + + await test.step("Add the small sweatpants to the cart", async () => { + await productPage.selectOption("S") + await productPage.addProductButton.click() + await expect(cartDropdown.navCartLink).toContainText("(2)") + const sweatpantsItem = await cartDropdown.getCartItem("Sweatpants", "S") + await expect(sweatpantsItem.locator).toBeVisible() + await expect(sweatpantsItem.variant).toHaveText("Variant: S") + await expect(sweatpantsItem.quantity).toContainText("1") + const sweatshirtItem = await cartDropdown.getCartItem("Sweatshirt", "S") + await expect(sweatshirtItem.locator).toBeVisible() + await expect(sweatshirtItem.quantity).toContainText("1") + await cartDropdown.goToCartButton.click() + await cartDropdown.close() + await cartPage.container.waitFor({ state: "visible" }) + }) + + await test.step("Verify the quantities in the cart", async () => { + const sweatpantsProduct = await cartPage.getProduct("Sweatpants", "S") + await expect(sweatpantsProduct.productRow).toBeVisible() + await expect(sweatpantsProduct.quantitySelect).toHaveValue("1") + const sweatshirtProduct = await cartPage.getProduct("Sweatshirt", "S") + await expect(sweatshirtProduct.productRow).toBeVisible() + await expect(sweatshirtProduct.quantitySelect).toHaveValue("1") + }) + }) + + test("Verify the prices carries over to checkout", async ({ + cartPage, + productPage, + storePage, + }) => { + await test.step("Navigate to the product page - go to the store page and click on the Hoodie product", async () => { + await storePage.goto() + const product = await storePage.getProduct("Hoodie") + await product.locator.click() + await productPage.container.waitFor({ state: "visible" }) + }) + + let hoodieSmallPrice = 0 + let hoodieMediumPrice = 0 + await test.step("Add the hoodie to the cart", async () => { + await productPage.selectOption("S") + hoodieSmallPrice = getFloatValue( + (await productPage.productPrice.getAttribute("data-value")) || "0" + ) + await productPage.clickAddProduct() + await productPage.cartDropdown.close() + await productPage.selectOption("M") + hoodieMediumPrice = getFloatValue( + (await productPage.productPrice.getAttribute("data-value")) || "0" + ) + await productPage.clickAddProduct() + + await productPage.cartDropdown.close() + }) + + await test.step("Navigate to another product - Longsleeve", async () => { + await storePage.goto() + const product = await storePage.getProduct("Longsleeve") + await product.locator.click() + await productPage.container.waitFor({ state: "visible" }) + }) + + let longsleeveSmallPrice = 0 + await test.step("Add the small longsleeve to the cart", async () => { + await productPage.selectOption("S") + longsleeveSmallPrice = getFloatValue( + (await productPage.productPrice.getAttribute("data-value")) || "0" + ) + await productPage.clickAddProduct() + await productPage.cartDropdown.close() + await productPage.selectOption("S") + await productPage.clickAddProduct() + await productPage.selectOption("S") + await productPage.clickAddProduct() + await productPage.cartDropdown.goToCartButton.click() + await productPage.cartDropdown.close() + await cartPage.container.waitFor({ state: "visible" }) + }) + + await test.step("Verify the price in the cart is the expected value", async () => { + const total = getFloatValue( + (await cartPage.cartSubtotal.getAttribute("data-value")) || "0" + ) + const calculatedTotal = + 3 * longsleeveSmallPrice + hoodieSmallPrice + hoodieMediumPrice + expect(compareFloats(total, calculatedTotal)).toBe(0) + }) + }) +}) diff --git a/e2e/utils/index.ts b/e2e/utils/index.ts new file mode 100644 index 000000000..b7f9f4b12 --- /dev/null +++ b/e2e/utils/index.ts @@ -0,0 +1,14 @@ +export function getFloatValue(s: string) { + return parseFloat(parseFloat(s).toFixed(2)) +} + +export function compareFloats(f1: number, f2: number) { + const diff = f1 - f2 + if (Math.abs(diff) < 0.01) { + return 0 + } else if (diff < 0) { + return -1 + } else { + return 1 + } +} diff --git a/src/lib/util/get-product-price.ts b/src/lib/util/get-product-price.ts index d15f6ea0c..0dcce4dbc 100644 --- a/src/lib/util/get-product-price.ts +++ b/src/lib/util/get-product-price.ts @@ -36,11 +36,13 @@ export function getProductPrice({ }) return { + calculated_price_number: cheapestVariant.calculated_price, calculated_price: formatAmount({ amount: cheapestVariant.calculated_price, region, includeTaxes: false, }), + original_price_number: cheapestVariant.original_price, original_price: formatAmount({ amount: cheapestVariant.original_price, region, @@ -68,11 +70,13 @@ export function getProductPrice({ } return { + calculated_price_number: variant.calculated_price, calculated_price: formatAmount({ amount: variant.calculated_price, region, includeTaxes: false, }), + original_price_number: variant.original_price, original_price: formatAmount({ amount: variant.original_price, region, diff --git a/src/modules/common/components/cart-totals/index.tsx b/src/modules/common/components/cart-totals/index.tsx index 7a2580a33..7ee2d1387 100644 --- a/src/modules/common/components/cart-totals/index.tsx +++ b/src/modules/common/components/cart-totals/index.tsx @@ -38,12 +38,18 @@ const CartTotals: React.FC = ({ data }) => { - {getAmount(subtotal)} + + {getAmount(subtotal)} + {!!discount_total && (
Discount - + - {getAmount(discount_total)}
@@ -51,24 +57,38 @@ const CartTotals: React.FC = ({ data }) => { {!!gift_card_total && (
Gift card - + - {getAmount(gift_card_total)}
)}
Shipping - {getAmount(shipping_total)} + + {getAmount(shipping_total)} +
Taxes - {getAmount(tax_total)} + + {getAmount(tax_total)} +
Total - {getAmount(total)} + + {getAmount(total)} +
diff --git a/src/modules/layout/components/cart-dropdown/index.tsx b/src/modules/layout/components/cart-dropdown/index.tsx index 8e0e91b9f..6b96327e9 100644 --- a/src/modules/layout/components/cart-dropdown/index.tsx +++ b/src/modules/layout/components/cart-dropdown/index.tsx @@ -133,8 +133,17 @@ const CartDropdown = ({ {item.title} - - Quantity: {item.quantity} + + + Quantity: {item.quantity} +
- + Remove @@ -158,7 +171,11 @@ const CartDropdown = ({ Subtotal{" "} (excl. taxes) - + {formatAmount({ amount: cartState.subtotal || 0, region: cartState.region, @@ -167,7 +184,11 @@ const CartDropdown = ({ - diff --git a/src/modules/products/components/mobile-actions/index.tsx b/src/modules/products/components/mobile-actions/index.tsx index 364370bc7..b7cc2a1e1 100644 --- a/src/modules/products/components/mobile-actions/index.tsx +++ b/src/modules/products/components/mobile-actions/index.tsx @@ -24,6 +24,7 @@ type MobileActionsProps = { handleAddToCart: () => void isAdding?: boolean show: boolean + optionsDisabled: boolean } const MobileActions: React.FC = ({ @@ -36,6 +37,7 @@ const MobileActions: React.FC = ({ handleAddToCart, isAdding, show, + optionsDisabled, }) => { const { state, open, close } = useToggleState() @@ -71,7 +73,10 @@ const MobileActions: React.FC = ({ leaveFrom="opacity-100" leaveTo="opacity-0" > -
+
{product.title} @@ -98,7 +103,12 @@ const MobileActions: React.FC = ({ )}
-
) diff --git a/src/modules/products/components/option-select/index.tsx b/src/modules/products/components/option-select/index.tsx index 40ae6c097..9c13463e4 100644 --- a/src/modules/products/components/option-select/index.tsx +++ b/src/modules/products/components/option-select/index.tsx @@ -9,7 +9,8 @@ type OptionSelectProps = { current: string updateOption: (option: Record) => void title: string - 'data-testid'?: string + disabled: boolean + "data-testid"?: string } const OptionSelect: React.FC = ({ @@ -17,14 +18,18 @@ const OptionSelect: React.FC = ({ current, updateOption, title, - 'data-testid': dataTestId + "data-testid": dataTestId, + disabled, }) => { const filteredOptions = option.values.map((v) => v.value).filter(onlyUnique) return (
Select {title} -
+
{filteredOptions.map((v) => { return (
) @@ -151,7 +154,7 @@ export default function ProductActions({
diff --git a/src/modules/products/components/product-price/index.tsx b/src/modules/products/components/product-price/index.tsx index f87f28799..daf7f8ed9 100644 --- a/src/modules/products/components/product-price/index.tsx +++ b/src/modules/products/components/product-price/index.tsx @@ -36,13 +36,24 @@ export default function ProductPrice({ })} > {!variant && "From "} - {selectedPrice.calculated_price} + + {selectedPrice.calculated_price} + {selectedPrice.price_type === "sale" && ( <>

Original: - {selectedPrice.original_price} + + {selectedPrice.original_price} +

-{selectedPrice.percentage_diff}% diff --git a/src/modules/products/templates/index.tsx b/src/modules/products/templates/index.tsx index cf3e7f727..6af20bbb9 100644 --- a/src/modules/products/templates/index.tsx +++ b/src/modules/products/templates/index.tsx @@ -29,7 +29,10 @@ const ProductTemplate: React.FC = ({ return ( <> -
+
@@ -40,13 +43,22 @@ const ProductTemplate: React.FC = ({
} + fallback={ + + } >
-
+
}>