DigitalConsumer.FixMyGlass/playwright-tests/pages/PaymentMethodPage.ts
maguire-arman 92c223487a Updating 836 with 631
Adds a new environment variable to skip content site checks, improving test environment flexibility.
Improves the "nextPage" function in BasePage to handle loader elements and button states more reliably.
Adds support for selecting loss location in CCPolicyInfoPage to reflect claim details accurately.
Updates assertion methods to use Soft assertions for smoother test execution.
Adds a new test case "InsuranceMeemicNearSchoolVerified" to expand test coverage.
2025-07-08 14:59:48 -04:00

495 lines
No EOL
25 KiB
TypeScript

import { expect, type Locator, type Page } from '@playwright/test';
import { BasePage } from './BasePage';
import { IPaymentDetails } from 'safelite-playwright-core';
import { AppointmentTimeslot, ServiceLocation, PaymentType, ServicePackage, VehicleDamage } from 'safelite-playwright-core';
import { PaymentMethod, ProgressBarPercentages } from 'framework/localTypes/Enums';
import { PaymentPage } from './PaymentPage';
import { AfterpayPage } from './AfterpayPage';
import { PaypalPage } from './PaypalPage';
import { ITestData } from 'framework/TestData';
import { step } from 'framework/localTypes/Step';
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 appointmentDetailsSection: 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.appointmentDetailsSection = this.page.locator('div .appt-details-snapshot');
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.cartPanelDetails.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,
flow, 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().then(
async () => {
await waitUntil(async () => {
return (await this.page.locator('.review-toggle.expanded').evaluate(el => window.getComputedStyle(el, ':after').transform)) === 'matrix(-1, 0, 0, -1, 0, 0)';
});
}
);
let expectedAppointmentDetails = new Map<string, string[]>();
expectedAppointmentDetails = await this.getExpectedVehicleDetails(testData, expectedAppointmentDetails);
expectedAppointmentDetails = await this.getExpectedVehicleDamage(testData, expectedAppointmentDetails);
expectedAppointmentDetails = await this.expectedServicePackageDetails(testData, expectedAppointmentDetails);
expectedAppointmentDetails = await this.getExpectedServiceLocation(testData, expectedAppointmentDetails);
expectedAppointmentDetails = await this.getExpectedAppointmentDate(testData, expectedAppointmentDetails);
expectedAppointmentDetails = await this.getExpectedCustomerDetails(testData, expectedAppointmentDetails);
let actualAppointmentDetails = await this.getActualAppointmentDetails();
for (const key in expectedAppointmentDetails) {
Soft.expect(actualAppointmentDetails[key]?.map(item => item.toLowerCase()))
.toEqual(expectedAppointmentDetails[key].map(item => item.toLowerCase()));
}
// Expand cart to see all details
await this.amountDueDropDown.click();
await this.reviewTable.waitFor({ state: "visible" });
// 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
Soft.expect(subtotalAmount).toBeGreaterThan(0);
// Final amount differs based on payment type
if (paymentDetails?.paymentType === PaymentType.PayAtService) {
Soft.expect(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 Soft.expect(servicePackageValue).toContain(`${servicePackage}`)
if (servicePackage === ServicePackage.Premium || servicePackage === ServicePackage.Standard) {
Soft.expect(servicePackageValue).toContain('New wiper blades');
}
if (servicePackage === ServicePackage.Premium) {
Soft.expect(servicePackageValue).toContain('Rain Repel Treatment');
}
// Promo Code Validation
if (paymentDetails!.promoCode) {
expect.soft(servicePackageValue).toContain(`Promo code ${paymentDetails!.promoCode} applied`);
};
// Early Bird line item validation
if (appointmentDetails?.appointmentTimeSlot == AppointmentTimeslot.EarlyBird) {
Soft.expect(servicePackageValue).toContain('Early bird');
}
}
}
/**
* 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");
}
}
async getActualAppointmentDetails(): Promise<any> {
let actualAppointmentDetails = new Map<string, string[]>();
let appointmentDetailsSubSections = await this.appointmentDetailsSection.locator('.review-table .py-3').all();
for (let element of appointmentDetailsSubSections) {
let label = await element.locator('div .text-block').innerText();
//added to trim the text to remove any leading or trailing spaces (example: "expert installation " to "expert installation")
let value = (await element.locator('.review-block-content').allInnerTexts() as string[]).map(item => item.trim());
actualAppointmentDetails[label] = value;
}
return actualAppointmentDetails;
}
async getExpectedVehicleDetails(testdata: Partial<ITestData>, expectedServicePackageDetails: Map<string, string[]>): Promise<any> {
const { vehicleDetails } = testdata;
expectedServicePackageDetails["Vehicle"] = [vehicleDetails?.year + " " + vehicleDetails?.make + " " + vehicleDetails?.model];
return expectedServicePackageDetails;
}
async getExpectedVehicleDamage(testdata: Partial<ITestData>, expectedServicePackageDetails: Map<string, string[]>): Promise<any> {
const {vehicleDamage} = testdata;
let vehicleDamageText: string[] = [];
if (vehicleDamage?.find((item: VehicleDamage) => item == VehicleDamage.WindshieldOneChip)) {
vehicleDamageText.push("Windshield repair - 1 chip");
}
else if (vehicleDamage?.find((item: VehicleDamage) => item == VehicleDamage.WindshieldTwoChips)) {
vehicleDamageText.push("Windshield repair - 2 chips");
}
else if (vehicleDamage?.find((item: VehicleDamage) => item == VehicleDamage.WindshieldThreeChips)) {
vehicleDamageText.push("Windshield repair - 3 chips");
}
if (vehicleDamage?.find((item: VehicleDamage) => item == VehicleDamage.WindshieldCrack)) {
vehicleDamageText.push("Windshield crack");
}
if (vehicleDamage?.find((item: VehicleDamage) => item == VehicleDamage.DriverVentGlass)) {
vehicleDamageText.includes("Side door - Driver side") ? vehicleDamageText[vehicleDamageText.length - 1 ] += "\nVent glass" : vehicleDamageText.push("Side door - Driver side", "Vent glass");
}
if (vehicleDamage?.find((item: VehicleDamage) => item == VehicleDamage.DriverFrontDoor)) {
vehicleDamageText.includes("Side door - Driver side") ? vehicleDamageText[vehicleDamageText.length - 1 ] += "\nFront door" : vehicleDamageText.push("Side door - Driver side", "Front door");
}
if (vehicleDamage?.find((item: VehicleDamage) => item == VehicleDamage.DriverRearDoor)) {
vehicleDamageText.includes("Side door - Driver side") ? vehicleDamageText[vehicleDamageText.length - 1 ] += "\nBack door" : vehicleDamageText.push("Side door - Driver side", "Back door");
}
if (vehicleDamage?.find((item: VehicleDamage) => item == VehicleDamage.DriverQuarterPanel)) {
vehicleDamageText.includes("Side door - Driver side") ? vehicleDamageText[vehicleDamageText.length - 1 ] += "\nQuarter panel" : vehicleDamageText.push("Side door - Driver side", "Quarter panel");
}
if (vehicleDamage?.find((item: VehicleDamage) => item == VehicleDamage.PassengerVentGlass)) {
vehicleDamageText.includes("Side door - Passenger side") ? vehicleDamageText[vehicleDamageText.length - 1 ] += "\nVent glass" : vehicleDamageText.push("Side door - Passenger side", "Vent glass");
}
if (vehicleDamage?.find((item: VehicleDamage) => item == VehicleDamage.PassengerFrontDoor)) {
vehicleDamageText.includes("Side door - Passenger side") ? vehicleDamageText[vehicleDamageText.length - 1 ] += "\nFront door" : vehicleDamageText.push("Side door - Passenger side", "Front door");
}
if (vehicleDamage?.find((item: VehicleDamage) => item == VehicleDamage.PassengerRearDoor)) {
vehicleDamageText.includes("Side door - Passenger side") ? vehicleDamageText[vehicleDamageText.length - 1 ] += "\nBack door" : vehicleDamageText.push("Side door - Passenger side", "Back door");
}
if (vehicleDamage?.find((item: VehicleDamage) => item == VehicleDamage.PassengerQuarterPanel)) {
vehicleDamageText.includes("Side door - Passenger side") ? vehicleDamageText[vehicleDamageText.length - 1 ] += "\nQuarter panel" : vehicleDamageText.push("Side door - Passenger side", "Quarter panel");
}
if (vehicleDamage?.find((item: VehicleDamage) => item == VehicleDamage.RearWindow || item == VehicleDamage.RearSliding)) {
vehicleDamageText.push("Rear window");
}
expectedServicePackageDetails["Damage"] = vehicleDamageText;
return expectedServicePackageDetails;
}
async expectedServicePackageDetails(testData: Partial<ITestData>, expectedServicePackageDetails: Map<string, string[]>): Promise<any> {
const {servicePackage} = testData;
let localStorage= JSON.parse(await this.page.evaluate('localStorage.getItem(\'vuex\')'));
let isRepair = localStorage.order.damage.isRepair as boolean;
let isInsurance = localStorage.order.payment.isInsurance as boolean;
let hasNonWindshieldGlass: boolean = false;
if (!isRepair) {
hasNonWindshieldGlass = localStorage.order.lineItems.glassParts.find((item: any) => item.partType !== "WINDSHIELD") ? true : false;
}
let isCaliforniaState = localStorage.order.serviceLocation.state as string == 'CA' ? true : false;
let hasRecalPart: boolean = false;
let canSafeliteRecalibrate: boolean = false;
if (!isRepair) {
hasRecalPart = localStorage.order.lineItems.glassParts.find((item: any) => item.requiresRecalibration === true) ? true : false;
canSafeliteRecalibrate = localStorage.order.lineItems.glassParts.find((item: any) => item.canSafeliteRecalibrate === true) ? true : false;
}
let recalRequired = !isRepair && (isInsurance || isCaliforniaState) && hasRecalPart && canSafeliteRecalibrate
let stringForRepair: string[] = ["Expert windshield repair", "Exclusive resin sealant", "Nationwide lifetime guarantee"];
let stringIfRecal = isRepair
? ""
: recalRequired
? " and recalibration"
: "";
let stringForReplace: string[] = [hasNonWindshieldGlass ? "New replacement glass" : "New replacement windshield", "Expert installation" + `${stringIfRecal}`, "Nationwide lifetime warranty"];
switch (servicePackage)
{
case ServicePackage.GlassOnly:
expectedServicePackageDetails["Glass service only"] = isRepair
? stringForRepair
: stringForReplace;
break;
case ServicePackage.Standard:
stringForRepair.push("New wiper blades");
stringForReplace.push("New wiper blades");
expectedServicePackageDetails["Standard service"] = isRepair
? stringForRepair
: stringForReplace;
break;
case ServicePackage.Premium:
stringForRepair.push("New wiper blades", "Rain repel treatment");
stringForReplace.push("New wiper blades", "Rain repel treatment");
expectedServicePackageDetails["Premium service"] = isRepair
? stringForRepair
: stringForReplace;
break;
}
return expectedServicePackageDetails;
}
async getExpectedServiceLocation(testData: Partial<ITestData>, expectedServicePackageDetails: Map<string, string[]>): Promise<any> {
const { appointmentDetails } = testData;
let serviceLocationTitle = appointmentDetails?.serviceLocation == ServiceLocation.Mobile
? "We're coming to you"
: "You're going to a Safelite shop";
let serviceLocation: string[] = [];
serviceLocation.push(
appointmentDetails?.serviceLocation == ServiceLocation.Mobile
? appointmentDetails?.serviceAddress
? appointmentDetails.serviceAddress.street + ", " + appointmentDetails.serviceAddress.city + ", " + appointmentDetails.serviceAddress.state + " " + appointmentDetails.serviceAddress.postalCode
: ""
: appointmentDetails?.shopAddress
? appointmentDetails.shopAddress
: ""
);
expectedServicePackageDetails[serviceLocationTitle] = serviceLocation;
return expectedServicePackageDetails;
}
async getExpectedAppointmentDate(testData: Partial<ITestData>, expectedServicePackageDetails: Map<string, string[]>): Promise<any> {
const { customerDetails, appointmentDetails } = testData;
let appointmentDateText: string[] = [];
const isMobileAppointment = appointmentDetails?.serviceLocation == ServiceLocation.Mobile
if (isMobileAppointment) {
let localStorage= JSON.parse(await this.page.evaluate('localStorage.getItem(\'vuex\')'));
let jobMinMinutes = localStorage.order.schedule.jobMinMinutes as number;
let jobMaxMinutes = localStorage.order.schedule.jobMaxMinutes as number;
if (jobMinMinutes < 60) {
customerDetails!.apptDuration = `${jobMinMinutes} - ${jobMaxMinutes} minutes`;
} else {
const minHours = Math.floor(jobMinMinutes / 60);
const maxHours = Math.floor(jobMaxMinutes / 60);
customerDetails!.apptDuration = `${minHours} - ${maxHours} hours`;
}
}
appointmentDateText.push(
customerDetails?.apptDate ? await this.getFormattedAppointmentDate(customerDetails.apptDate) + " " + customerDetails?.apptTime?.replace("-", "—") : "",
customerDetails?.apptDuration ? "Estimated appointment length: " + customerDetails.apptDuration : ""
);
expectedServicePackageDetails["Appointment date + time"] = appointmentDateText;
return expectedServicePackageDetails;
}
async getExpectedCustomerDetails(testData: Partial<ITestData>, expectedServicePackageDetails: Map<string, string[]>): Promise<any> {
const { customerDetails } = testData;
let customerDetailsText: string[] = [];
customerDetailsText.push(
customerDetails?.firstName.toUpperCase() + " " + customerDetails?.lastName.toUpperCase(),
customerDetails?.email ? customerDetails?.email.toUpperCase() : "",
customerDetails?.phoneNumber ? customerDetails?.phoneNumber : "",
"Opted out of text message updates"
);
expectedServicePackageDetails["Contact details"] = customerDetailsText;
return expectedServicePackageDetails;
}
async getFormattedAppointmentDate(appointmentDate: string) {
// Parse the original date
let parsedAppointmentDate = new Date(`${appointmentDate}` + 'T00:00:00');
// Format the new date as a string
let updatedAppointmentDate = parsedAppointmentDate.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
return updatedAppointmentDate;
}
@step("PaymentMethodPage >> Select Payment Method: ")
async handlePaymentMethodPage(testData: Partial<ITestData>) {
const { servicePackage, isRecalVehicle, paymentDetails } = testData;
await this.validateProgressBar(ProgressBarPercentages.PaymentMethodPage);
await this.validatePaymentDetailsPage(testData);
// Verify VAPS wipers on backend for standard and premium packages
if (servicePackage === ServicePackage.Standard || servicePackage === ServicePackage.Premium) {
await this.verifyVAPS();
}
if (paymentDetails?.paymentType) {
await this.executePayment(paymentDetails!, isRecalVehicle!);
} else {
await this.nextPage();
}
}
}