DigitalConsumer.FixMyGlass/playwright-tests/pages/PaymentMethodPage.ts
Scott Kiener de2f501773 Revert "Merge pull request #2458 from Safelite/feature/CASH-530"
This reverts commit dafc79cd64, reversing
changes made to 8ae73d4b6b.
2025-04-30 10:01:27 -04:00

342 lines
No EOL
16 KiB
TypeScript

import { expect, type Locator, type Page } from '@playwright/test';
import { BasePage } from './BasePage';
import { IPaymentDetails } from '@business-logic/types/CustomerDetails';
import { PaymentMethod, PaymentType, ServicePackage, VehicleDamage } from '@business-logic/types/Enums';
import { PaymentPage } from './PaymentPage';
import { AfterpayPage } from './AfterpayPage';
import { PaypalPage } from './PaypalPage';
import { ITestData } from '@business-logic/types/ITestData';
export class PaymentMethodPage extends BasePage {
readonly page: Page;
readonly payAtServiceButton: Locator;
// readonly paypalButton: Locator;
// readonly creditCardButton: Locator;
readonly payNowButton: Locator;
readonly payWithInsuranceButton: Locator;
readonly payInFourButton: Locator;
readonly amountDueTextField: Locator;
readonly amountDueDropDown: Locator;
readonly appointmentDetailsDropdown: Locator;
readonly subtotalAmountTextField: Locator;
readonly submitButton: Locator;
readonly recalibrationCheckbox: Locator;
readonly paymentPage: PaymentPage;
readonly paypalPage: PaypalPage;
// Payment detail page validation locators
readonly reviewTable: Locator;
readonly vehicleSection: Locator;
readonly damageSection: Locator;
readonly serviceDetailsSection: Locator;
readonly serviceLocationSection: Locator;
readonly appointmentDateSection: Locator;
readonly contactDetailsSection: Locator;
readonly cartPanelDetails: Locator;
readonly subtotalText: Locator;
readonly finalAmountDueText: Locator;
readonly glassServiceText: Locator;
readonly wiperBladesText: Locator;
readonly rainDefenseText: Locator;
readonly deductibleText: Locator;
url = process.env['BASE_URL']! + '/fmg/?fmgPage=payment-method';
constructor(page: Page) {
super(page);
this.page = page;
this.payAtServiceButton = this.page.locator('[buttonlabel="Pay at my appointment"]'); //this.page.getByText('Pay at time of service');
this.amountDueTextField = this.page.getByLabel('expand cart').locator('.amount-due');
this.amountDueDropDown = this.page.getByLabel('expand cart');
this.appointmentDetailsDropdown = this.page.getByLabel('expand appointment details');
this.payWithInsuranceButton = this.page.locator('div').filter({ hasText: /^Pay with insurance$/ }).nth(1);
this.subtotalAmountTextField = this.page.locator('.sub-total span').nth(1);
this.payNowButton = this.page.locator('[buttonlabel="Pay now"]');
this.payInFourButton = this.page.locator('[buttonlabel="Pay in 4 installments"]');
this.submitButton = this.page.locator('[data-test-id="nav-bar-main-button"]');
this.recalibrationCheckbox = this.page.getByLabel('I understand after windshield');
// this.creditCardButton = page.locator('div').filter({ hasText: /^Credit or Debit$/ }).nth(1);
this.paymentPage = new PaymentPage(page);
this.paypalPage = new PaypalPage(page);
// Payment details validation locators
this.reviewTable = this.page.locator('div.review-table');
// Section locators - find by heading text
this.vehicleSection = this.page.locator('.review-table').locator('div', { hasText: 'Vehicle' }).first();
this.damageSection = this.page.locator('.review-table').locator('div', { hasText: 'Damage' }).first();
this.serviceDetailsSection = this.page.locator('.review-table').locator('div', { hasText: 'Glass service only' }).first();
this.serviceLocationSection = this.page.locator('.review-table').locator('div', { hasText: "We're coming to you" }).first();
this.appointmentDateSection = this.page.locator('.review-table').locator('div', { hasText: 'Appointment date + time' }).first();
this.contactDetailsSection = this.page.locator('.review-table').locator('div', { hasText: 'Contact details' }).first();
// Cart panel elements
this.cartPanelDetails = this.page.locator('.cart-panel');
this.subtotalText = this.page.locator('.sub-total');
this.finalAmountDueText = this.page.locator('div.amount-due');
this.glassServiceText = this.page.locator('div', { hasText: /^Glass service only$/ });
this.wiperBladesText = this.page.locator('div', { hasText: /^New wiper blades$/ });
this.rainDefenseText = this.page.locator('div', { hasText: /^Rain Defense™$/ });
this.deductibleText = this.page.locator('#deductible-value');
}
async validatePaymentDetailsPage(testData: Partial<ITestData>) {
// Destructure data we use
const { vehicleDetails, customerDetails, servicePackage, promoCode,
isPolicyFound, claimDetails, paymentDetails, appointmentDetails,
isUseVehicleOnPolicy, paymentMethod, vehicleDamage } = testData;
// Wait for review table to be visible to ensure page is loaded
await this.appointmentDetailsDropdown.waitFor({state: "visible"});
await this.appointmentDetailsDropdown.click(); // Expand cart to see all details
await this.amountDueDropDown.click();
await this.reviewTable.waitFor({ state: "visible" });
// Helper method to get content lines from a section
const getSectionContent = async (section: Locator) => {
// First make sure section exists
if (await section.count() === 0) return [];
// Find all content lines within this section
const contentLines = await section.locator('div.small.review-block-content').allInnerTexts();
return contentLines;
};
// Validate vehicle information
const vehicleContent = await getSectionContent(this.vehicleSection);
if (vehicleContent.length > 0) {
const vehicleText = vehicleContent[0];
expect.soft(vehicleText).toContain(`${vehicleDetails?.year} ${vehicleDetails?.make} ${vehicleDetails?.model}`);
}
// Validate damage type
// TODO: Work out logic on how to verify vehicle Damage in payment details with vehicleDamage
// const damageContent = await getSectionContent(this.damageSection);
// if (vehicleDamage && damageContent.length > 0) {
// const damageText = damageContent[0];
// // For each damage type in the array, check if its display text is in the damage content
// for (const damage of vehicleDamage) {
// const expectedDamageText = this.getDamageDisplayText(damage);
// // If this is a single damage item, it should match exactly
// if (vehicleDamage.length === 1) {
// expect.soft(damageText).toContain(expectedDamageText);
// } else {
// // For multiple damages, check if any of the damage content lines contain this damage type
// const damageFound = damageContent.some(content =>
// content.includes(expectedDamageText)
// );
// expect.soft(damageFound).toBeTruthy();
// }
// }
// }
// Validate service details based on package
const serviceContent = await getSectionContent(this.serviceDetailsSection);
if (servicePackage && serviceContent.length > 0) {
//TODO: Add service package validation
// move over logic from Kishan's code
// Additional validations for Standard and Premium packages
if (servicePackage === ServicePackage.Premium || servicePackage === ServicePackage.Standard) {
await expect.soft(this.wiperBladesText).toBeVisible();
}
if (servicePackage === ServicePackage.Premium) {
await expect.soft(this.rainDefenseText).toBeVisible();
}
}
// Validate service location
const locationContent = await getSectionContent(this.serviceLocationSection);
if (appointmentDetails?.serviceLocation && locationContent.length > 0) {
// Find the title element of the service location section
const serviceLocationTitle = this.serviceLocationSection.locator('span').first();
const serviceLocationValue = await serviceLocationTitle.innerText();
// Check for mobile/inshop service wording
if (appointmentDetails.serviceLocation.toString().includes('Mobile')) {
expect.soft(serviceLocationValue).toContain("We're coming to you");
} else if (appointmentDetails.serviceLocation.toString().includes('InShop')) {
expect.soft(serviceLocationValue).toContain("Bring to shop");
}
// Validate address if available
if (appointmentDetails.serviceAddress && locationContent.length > 0) {
const addressText = locationContent[0].toLowerCase();
expect.soft(addressText).toContain(appointmentDetails.serviceAddress.street.toLowerCase());
}
}
// Validate appointment date/time
const appointmentContent = await getSectionContent(this.appointmentDateSection);
if (customerDetails?.apptDate && appointmentContent.length > 0) {
const appointmentText = appointmentContent[0];
expect.soft(appointmentText).toContain(customerDetails.apptDate);
}
// Validate contact details
const contactContent = await getSectionContent(this.contactDetailsSection);
if (customerDetails && contactContent.length > 0) {
const fullName = `${customerDetails.firstName} ${customerDetails.lastName}`;
const email = customerDetails.email;
const phone = customerDetails.phoneNumber;
// Check if contact details are present
const contactTextJoined = contactContent.join(' ');
expect.soft(contactTextJoined).toContain(fullName);
expect.soft(contactTextJoined).toContain(email);
expect.soft(contactTextJoined).toContain(phone);
}
// Cart Validation
// Get pricing information from cart panel
if (await this.subtotalText.isVisible()) {
const subtotalValue = await this.subtotalText.innerText();
const finalAmountDueValue = await this.finalAmountDueText.innerText();
// Pricing validations differ by payment method
if (paymentMethod === PaymentMethod.SelfPay && paymentDetails?.paymentType !== PaymentType.PayWithInsurance) {
// Extract amounts for self-pay customers
const subtotalAmount = this.extractAmount(subtotalValue);
const finalAmountDueAmount = this.extractAmount(finalAmountDueValue);
// Subtotal should be greater than 0
expect.soft(subtotalAmount).toBeGreaterThan(0);
// Final amount differs based on payment type
if (paymentDetails?.paymentType === PaymentType.PayAtService) {
expect.soft(finalAmountDueAmount).toBeGreaterThan(0);
} else if (paymentDetails?.paymentType === PaymentType.Credit ||
paymentDetails?.paymentType === PaymentType.Paypal ||
paymentDetails?.paymentType === PaymentType.AfterPay) {
// For payment types that charge immediately, amount due could be 0
// This logic might need adjusting based on actual business rules
}
} else {
// For insurance payments, check for proper indicators
// TODO: Apply logic for deductible and insurance payment logic
}
// Service package validations
const servicePackageValue = await this.cartPanelDetails.textContent();
await expect.soft(this.cartPanelDetails).toContainText(`${servicePackage}`)
if (servicePackage === ServicePackage.Premium || servicePackage === ServicePackage.Standard) {
expect.soft(servicePackageValue).toContain('New wiper blades');
}
if (servicePackage === ServicePackage.Premium) {
expect.soft(servicePackageValue).toContain('Rain Defense™');
}
// Promo Code Validation
if (promoCode) {
expect.soft(servicePackageValue).toContain(`Promo code ${promoCode} applied`);
};
}
}
/**
* Helper method to extract numeric amount from price strings like "$123.45"
*/
private extractAmount(valueString: string): number {
const matches = valueString.match(/\$([0-9,]+(\.[0-9]{2})?)/);
if (matches && matches[1]) {
return parseFloat(matches[1].replace(/,/g, ''));
}
return 0;
}
async getAmountDue(): Promise<string> {
await expect(this.amountDueTextField).toBeVisible();
const text = await this.amountDueTextField.textContent();
if (!text) throw new Error("Amount due text is empty");
return text;
}
async getSubtotal(): Promise<string> {
await this.amountDueDropDown.click();
await expect(this.subtotalAmountTextField).toBeVisible();
const text = await this.subtotalAmountTextField.textContent();
if (!text) throw new Error("Subtotal text is empty");
return text;
}
async executePayment(paymentDetails: IPaymentDetails, isRecalVehicle: boolean) {
const browserContext = this.page.context();
switch(paymentDetails.paymentType) {
case PaymentType.Credit:
await this.selectCreditCard();
await this.nextPage();
await this.paymentPage.populateCreditCardDetails(paymentDetails);
await this.nextPage();
break;
case PaymentType.AfterPay:
await this.payInFourButton.click();
await this.nextPage();
// Capture popup
const afterpayPopup = await browserContext.waitForEvent('page');
const afterpayPage = new AfterpayPage(afterpayPopup);
// Execute payment
await afterpayPage.executeAfterpayPayment(paymentDetails);
break;
case PaymentType.Paypal:
await this.selectPaypal();
// TODO: Click paypal button
await this.nextPage();
await this.paymentPage.navigateToPaypalCheckout();
await this.paypalPage.completePaypalPurchase(paymentDetails);
break;
case PaymentType.PayAtService:
await this.selectPayAtService(isRecalVehicle);
await this.nextPage();
break;
case PaymentType.PayWithInsurance:
await this.payWithInsuranceButton.click();
await this.nextPage();
break;
default:
console.error('PaymentMethodPage >> Logic for this payment method unimplemented');
break;
}
}
async selectPaypal(){
await this.payNowButton.click();
}
l
async selectCreditCard(){
await this.payNowButton.click();
}
async selectPayAtService(isRecalVehicle: boolean){
if (await this.payAtServiceButton.isVisible()) {
await this.payAtServiceButton.click();
} else if (isRecalVehicle) {
await this.recalibrationCheckbox.click();
}
}
async verifyVAPS(): Promise<void> {
// Get Vuex state from localStorage
const vuexState = JSON.parse(await this.page.evaluate('localStorage.getItem(\'vuex\')'));
// Validate the count of FRONT WIPER parts
if (vuexState.order?.lineItems?.vaps?.length > 0) {
const frontWiperPartsCount = vuexState.order.lineItems.vaps.filter(vap =>
vap.partType === "FRONT WIPER"
).length;
await expect(frontWiperPartsCount).toBe(2);
} else {
throw new Error("No VAPS found in the order");
}
}
}