All files / pages CartPage.js

98.13% Statements 158/161
100% Branches 22/22
92.85% Functions 13/14
98.13% Lines 158/161

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 1621x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 1x 1x 1x 1x 1x       1x 1x 1x 1x 1x 19x 19x 19x 1x 1x 1x 1x 1x 7x 7x 7x 7x 7x 14x 14x 7x 7x 7x 7x 1x 1x 1x 1x 1x 2x 2x 2x 2x 2x 1x 1x 1x 1x 1x 1x 9x 9x 1x 1x 1x 1x 1x 1x 4x 4x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 3x 3x 3x 1x 1x 2x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x  
// @ts-check
const { expect } = require('@playwright/test');
const { BasePage } = require('./BasePage');
const { HeaderComponent } = require('./components/HeaderComponent');
const { environment } = require('../config/environment');
 
/**
 * CartPage POM handles interactions with the shopping cart (/view_cart).
 */
class CartPage extends BasePage {
  /**
   * @param {import('@playwright/test').Page} page
   */
  constructor(page) {
    super(page);
    this.header = new HeaderComponent(page);
 
    this.cartTable = page.locator('#cart_info_table');
    this.cartRows = page.locator('#cart_info_table tbody tr');
    this.proceedToCheckoutButton = page.locator('a.check_out');
 
    // Modal shown when unauthenticated user attempts checkout
    this.checkoutModal = page.locator('#checkoutModal');
    this.registerLoginModalLink = page.locator('#checkoutModal a[href="/login"]');
 
    // Cart Footer & Subscription
    this.footer = page.locator('#footer');
    this.subscriptionHeading = page.locator('#footer').getByRole('heading', { name: 'Subscription', exact: false });
    this.subscriptionInput = page.locator('#susbscribe_email');
    this.subscribeButton = page.locator('#subscribe');
    this.subscriptionSuccessAlert = page.locator('#success-subscribe');
  }
 
  /**
   * Navigates directly to /view_cart
   */
  async navigate() {
    await this.goto(environment.routes.cart);
    await this.verifyLoaded();
  }
 
  /**
   * Verifies the cart page is loaded.
   */
  async verifyLoaded() {
    await expect(this.page).toHaveURL(new RegExp(`${environment.routes.cart}`));
    await expect(this.page.locator('#cart_items')).toBeVisible({ timeout: 10000 });
  }
 
  /**
   * Clicks 'Proceed To Checkout'.
   */
  async proceedToCheckout() {
    await this.verifyLoaded();
    await this.proceedToCheckoutButton.scrollIntoViewIfNeeded();
 
    // Robust retry to guard against dropped clicks prior to jQuery event attachment
    await expect.poll(async () => {
      if (this.page.url().includes('/checkout')) return true;
      if (await this.checkoutModal.isVisible()) return true;
      await this.proceedToCheckoutButton.click();
      return false;
    }, { timeout: 15000, intervals: [500, 1000, 1500] }).toBeTruthy();
  }
 
  /**
   * Clicks 'Register / Login' from checkout modal.
   */
  async clickRegisterLoginFromModal() {
    await expect(this.checkoutModal).toBeVisible({ timeout: 10000 });
    const modalLink = this.page.locator('#checkoutModal').getByRole('link', { name: 'Register / Login' });
    await expect(modalLink).toBeVisible({ timeout: 10000 });
    await modalLink.click();
  }
 
  /**
   * Returns locator for a specific product cart row.
   * @param {string | number} productId
   */
  getProductRow(productId) {
    return this.page.locator(`#product-${productId}`);
  }
 
  /**
   * Asserts a product is visible in the cart.
   * @param {string | number} productId
   */
  async verifyProductInCart(productId) {
    await expect(this.getProductRow(productId)).toBeVisible();
  }
 
  /**
   * Asserts a product is removed from the cart.
   * @param {string | number} productId
   */
  async verifyProductRemoved(productId) {
    await expect(this.getProductRow(productId)).toHaveCount(0);
  }
 
  /**
   * Gets the displayed quantity for a product.
   * @param {string | number} productId
   * @returns {Promise<string>}
   */
  async getProductQuantity(productId) {
    const row = this.getProductRow(productId);
    return (await row.locator('.cart_quantity').innerText()).trim();
  }
 
  /**
   * Asserts that line item total equals price * quantity.
   * @param {string | number} productId
   */
  async verifyProductTotalCalculation(productId) {
    const row = this.getProductRow(productId);
    await expect(row).toBeVisible();
 
    const priceText = await row.locator('.cart_price').innerText();
    const quantityText = await row.locator('.cart_quantity').innerText();
    const totalText = await row.locator('.cart_total').innerText();
 
    const price = Number(priceText.replace(/[^0-9.]/g, ''));
    const quantity = Number(quantityText.trim());
    const total = Number(totalText.replace(/[^0-9.]/g, ''));
 
    expect(quantity).toBeGreaterThan(0);
    expect(total).toBe(price * quantity);
  }
 
  /**
   * Deletes a product from the cart by its product ID.
   * @param {string | number} productId
   */
  async removeProduct(productId) {
    const row = this.getProductRow(productId);
    const deleteButton = row.locator('.cart_quantity_delete');
 
    await expect.poll(async () => {
      const count = await row.count();
      if (count === 0) return true;
      if (await deleteButton.isVisible()) {
        await deleteButton.click();
      }
      return false;
    }, { timeout: 15000, intervals: [500, 1000, 1500] }).toBeTruthy();
  }
 
  /**
   * Scrolls to footer and submits subscription form on cart page.
   * @param {string} email
   */
  async subscribe(email) {
    await this.footer.scrollIntoViewIfNeeded();
    await expect(this.subscriptionHeading).toBeVisible();
    await this.subscriptionInput.fill(email);
    await this.subscribeButton.click();
    await expect(this.subscriptionSuccessAlert).toContainText('You have been successfully subscribed!');
  }
}
 
module.exports = { CartPage };